@abloatai/ablo 0.10.1 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +63 -23
  3. package/dist/BaseSyncedStore.d.ts +75 -0
  4. package/dist/BaseSyncedStore.js +193 -8
  5. package/dist/Database.d.ts +10 -2
  6. package/dist/Database.js +15 -1
  7. package/dist/SyncClient.d.ts +12 -1
  8. package/dist/SyncClient.js +110 -26
  9. package/dist/agent/Agent.d.ts +9 -9
  10. package/dist/agent/Agent.js +16 -16
  11. package/dist/agent/index.d.ts +1 -1
  12. package/dist/agent/index.js +2 -2
  13. package/dist/agent/types.d.ts +1 -1
  14. package/dist/agent/types.js +1 -1
  15. package/dist/ai-sdk/{intent-broadcast.d.ts → claim-broadcast.d.ts} +10 -10
  16. package/dist/ai-sdk/{intent-broadcast.js → claim-broadcast.js} +6 -6
  17. package/dist/ai-sdk/coordination-context.d.ts +9 -9
  18. package/dist/ai-sdk/coordination-context.js +8 -8
  19. package/dist/ai-sdk/index.d.ts +1 -1
  20. package/dist/ai-sdk/index.js +1 -1
  21. package/dist/ai-sdk/wrap.d.ts +4 -4
  22. package/dist/ai-sdk/wrap.js +4 -4
  23. package/dist/api/index.d.ts +2 -2
  24. package/dist/cli.cjs +369 -67
  25. package/dist/client/Ablo.d.ts +30 -63
  26. package/dist/client/Ablo.js +124 -103
  27. package/dist/client/ApiClient.d.ts +6 -5
  28. package/dist/client/ApiClient.js +86 -62
  29. package/dist/client/auth.d.ts +9 -4
  30. package/dist/client/auth.js +40 -5
  31. package/dist/client/createModelProxy.d.ts +41 -54
  32. package/dist/client/createModelProxy.js +123 -20
  33. package/dist/client/httpClient.d.ts +2 -0
  34. package/dist/client/httpClient.js +1 -1
  35. package/dist/client/index.d.ts +3 -3
  36. package/dist/client/writeOptionsSchema.d.ts +4 -4
  37. package/dist/client/writeOptionsSchema.js +4 -4
  38. package/dist/coordination/schema.d.ts +249 -38
  39. package/dist/coordination/schema.js +172 -39
  40. package/dist/core/index.d.ts +2 -2
  41. package/dist/core/index.js +4 -4
  42. package/dist/errorCodes.d.ts +9 -9
  43. package/dist/errorCodes.js +16 -16
  44. package/dist/errors.d.ts +51 -2
  45. package/dist/errors.js +94 -5
  46. package/dist/interfaces/index.d.ts +8 -4
  47. package/dist/policy/index.d.ts +1 -1
  48. package/dist/policy/types.d.ts +13 -13
  49. package/dist/policy/types.js +8 -8
  50. package/dist/react/AbloProvider.d.ts +51 -4
  51. package/dist/react/AbloProvider.js +95 -11
  52. package/dist/react/context.d.ts +26 -9
  53. package/dist/react/context.js +2 -2
  54. package/dist/react/index.d.ts +4 -4
  55. package/dist/react/index.js +4 -4
  56. package/dist/react/useAblo.js +5 -5
  57. package/dist/react/{useIntent.d.ts → useClaim.d.ts} +9 -9
  58. package/dist/react/useClaim.js +42 -0
  59. package/dist/schema/index.js +1 -1
  60. package/dist/schema/schema.d.ts +3 -3
  61. package/dist/schema/sugar.d.ts +3 -3
  62. package/dist/schema/sugar.js +3 -3
  63. package/dist/schema/sync-delta-wire.d.ts +8 -8
  64. package/dist/server/commit.d.ts +2 -2
  65. package/dist/sync/AreaOfInterestManager.d.ts +162 -0
  66. package/dist/sync/AreaOfInterestManager.js +233 -0
  67. package/dist/sync/BootstrapHelper.d.ts +9 -1
  68. package/dist/sync/BootstrapHelper.js +15 -5
  69. package/dist/sync/NetworkProbe.d.ts +1 -1
  70. package/dist/sync/NetworkProbe.js +1 -1
  71. package/dist/sync/SyncWebSocket.d.ts +59 -25
  72. package/dist/sync/SyncWebSocket.js +123 -26
  73. package/dist/sync/awaitClaimGrant.d.ts +40 -0
  74. package/dist/sync/awaitClaimGrant.js +86 -0
  75. package/dist/sync/createClaimStream.d.ts +34 -0
  76. package/dist/sync/{createIntentStream.js → createClaimStream.js} +92 -81
  77. package/dist/sync/createPresenceStream.js +3 -2
  78. package/dist/sync/participants.d.ts +10 -10
  79. package/dist/sync/participants.js +17 -10
  80. package/dist/sync/schemas.d.ts +8 -8
  81. package/dist/transactions/TransactionQueue.d.ts +23 -0
  82. package/dist/transactions/TransactionQueue.js +186 -12
  83. package/dist/types/global.d.ts +18 -13
  84. package/dist/types/global.js +11 -6
  85. package/dist/types/index.d.ts +9 -7
  86. package/dist/types/index.js +2 -2
  87. package/dist/types/streams.d.ts +114 -98
  88. package/dist/types/streams.js +1 -1
  89. package/dist/utils/asyncIterator.d.ts +1 -1
  90. package/dist/utils/asyncIterator.js +1 -1
  91. package/dist/wire/frames.d.ts +2 -2
  92. package/docs/api.md +3 -3
  93. package/docs/client-behavior.md +6 -3
  94. package/docs/coordination.md +13 -3
  95. package/docs/data-sources.md +29 -9
  96. package/docs/migration.md +40 -0
  97. package/docs/quickstart.md +61 -33
  98. package/docs/react.md +46 -0
  99. package/llms-full.txt +25 -8
  100. package/llms.txt +11 -9
  101. package/package.json +3 -2
  102. package/dist/react/useIntent.js +0 -42
  103. package/dist/sync/awaitIntentGrant.d.ts +0 -40
  104. package/dist/sync/awaitIntentGrant.js +0 -62
  105. package/dist/sync/createIntentStream.d.ts +0 -34
@@ -3,14 +3,14 @@
3
3
  *
4
4
  * Ablo treats humans and agents as participants on live application
5
5
  * entities. Participants announce what they are reading or editing,
6
- * claim intent before writing, and capture context watermarks before
6
+ * claim before writing, and capture context watermarks before
7
7
  * long-running AI work. The customer keeps their own schema, agent
8
8
  * stack, tools, prompts, and product policy; the sync engine provides
9
9
  * the shared coordination substrate.
10
10
  */
11
11
  import type { InferModel, Schema } from '../schema/schema.js';
12
- import type { TargetRange, OnStaleMode, IntentClaim, PresenceKind } from '../coordination/schema.js';
13
- export type { TargetRange, OnStaleMode, IntentClaim, PresenceKind };
12
+ import type { TargetRange, OnStaleMode, WireClaim, ClaimRejection, PresenceKind, ParticipantKind } from '../coordination/schema.js';
13
+ export type { TargetRange, OnStaleMode, WireClaim, ClaimRejection, PresenceKind, ParticipantKind };
14
14
  /**
15
15
  * Any JSON-serializable value. Used where the SDK accepts free-form
16
16
  * metadata that will be persisted / transported as JSON — avoids
@@ -290,15 +290,15 @@ export interface Activity {
290
290
  * writes this shape only.
291
291
  */
292
292
  export interface Peer {
293
- readonly participantKind: 'human' | 'agent';
293
+ readonly participantKind: ParticipantKind;
294
294
  readonly participantId: string;
295
295
  readonly label?: string;
296
296
  readonly syncGroups: readonly string[];
297
297
  readonly activity: Activity;
298
298
  /** Server timestamp of the most recent frame from this participant. */
299
299
  readonly lastActive: string;
300
- /** Pending-mutation intents this participant has declared. */
301
- readonly activeIntents?: ReadonlyArray<IntentClaim>;
300
+ /** Pending-mutation claims this participant has declared. */
301
+ readonly activeClaims?: ReadonlyArray<Claim>;
302
302
  }
303
303
  /** Outbound `presence_update` payload. */
304
304
  export interface PresenceUpdatePayload {
@@ -307,24 +307,24 @@ export interface PresenceUpdatePayload {
307
307
  readonly isAgent?: boolean;
308
308
  }
309
309
  /**
310
- * Intent broadcasts — "I'm about to do X on Y." Broadcasts flow on
310
+ * Claim broadcasts — "I'm about to do X on Y." Broadcasts flow on
311
311
  * the same WS as presence, so every participant sees them in real
312
- * time. Cooperative mutex: the intent doesn't enforce exclusion; it
312
+ * time. Cooperative mutex: the claim doesn't enforce exclusion; it
313
313
  * announces. Other agents observe and yield. This is cheaper and
314
314
  * more flexible than a central lock table and composes with presence.
315
315
  */
316
316
  /**
317
- * Options common to every verb-style intent announcement
318
- * (`intents.analyzing`, `.drafting`, etc.).
317
+ * Options common to every verb-style claim announcement
318
+ * (`claims.analyzing`, `.drafting`, etc.).
319
319
  *
320
320
  * The one required field is the *target* — everything else is a
321
- * sensible default. Prefer the verb methods in `IntentStream` below
321
+ * sensible default. Prefer the verb methods in `ClaimStream` below
322
322
  * (`analyzing(entity, { ttl: '3m' })`) over the raw `announce(...)`
323
323
  * escape hatch.
324
324
  */
325
- export interface IntentOptions {
325
+ export interface ClaimLeaseOptions {
326
326
  /**
327
- * How long before the server auto-expires this intent if the
327
+ * How long before the server auto-expires this claim if the
328
328
  * participant doesn't finish the work. Accepts either a number (in
329
329
  * seconds — back-compat with `ttlSeconds`) or a duration string:
330
330
  * `'500ms'`, `'30s'`, `'3m'`, `'24h'`.
@@ -333,7 +333,7 @@ export interface IntentOptions {
333
333
  }
334
334
  /** Re-export of the duration helper shape. See `./duration.ts`. */
335
335
  export type Duration = import('../utils/duration.js').Duration;
336
- export interface ClaimOptions extends IntentOptions {
336
+ export interface ClaimOptions extends ClaimLeaseOptions {
337
337
  /**
338
338
  * Free-form reason describing why you're claiming. Surfaces in conflict
339
339
  * messages and the activity overlay. Defaults to `'editing'`. Common
@@ -349,21 +349,21 @@ export interface ClaimOptions extends IntentOptions {
349
349
  readonly description?: string;
350
350
  /**
351
351
  * Join the server's fair FIFO queue on contention instead of being
352
- * rejected. The grant arrives asynchronously (`intent_acquired` if the
353
- * target was free, `intent_granted` once promoted to the head of the line).
352
+ * rejected. The grant arrives asynchronously (`claim_acquired` if the
353
+ * target was free, `claim_granted` once promoted to the head of the line).
354
354
  * The low-level `claim` returns its handle immediately regardless; callers
355
355
  * that need to *wait* for the grant use the awaiting wrappers
356
- * (`ablo.<model>.claim`), which pair this flag with `awaitIntentGrant`.
356
+ * (`ablo.<model>.claim`), which pair this flag with `awaitClaimGrant`.
357
357
  */
358
358
  readonly queue?: boolean;
359
359
  }
360
- export interface IntentStream {
360
+ export interface ClaimStream {
361
361
  /**
362
- * Claim an exclusive intent on a target. Returns a handle — call
362
+ * Claim an exclusive claim on a target. Returns a handle — call
363
363
  * `.revoke()` to cancel, let it expire via TTL, or use `await using`
364
364
  * (TC39 explicit resource management) to auto-revoke on scope exit.
365
365
  *
366
- * Server rejects via `intent_rejected` when another participant
366
+ * Server rejects via `claim_rejected` when another participant
367
367
  * already holds a claim on the same target. Default `reason` is
368
368
  * `'editing'`; pass `{reason: 'writing'}` (or any string) to override.
369
369
  *
@@ -372,30 +372,30 @@ export interface IntentStream {
372
372
  * scoped `claim(reason, opts)` overload were collapsed into this
373
373
  * single primitive.
374
374
  */
375
- claim(target: PresenceTarget, opts?: ClaimOptions): Claim;
375
+ claim(target: PresenceTarget, opts?: ClaimOptions): ClaimHandle;
376
376
  /**
377
- * Reactive view of every other participant's active intents.
377
+ * Reactive view of every other participant's active claims.
378
378
  * Reads return the current snapshot; pair with `subscribe(...)`
379
379
  * below to get notified on change.
380
380
  */
381
- readonly others: ReadonlyArray<ActiveIntent>;
381
+ readonly others: ReadonlyArray<ActiveClaim>;
382
382
  /**
383
383
  * Reactive view of the wait queue on one target — the FIFO line of
384
- * `status: 'queued'` intents behind the current holder, each with its
384
+ * `status: 'queued'` claims behind the current holder, each with its
385
385
  * `action`, `heldBy`, and `position`. Synced from the server's per-entity
386
- * `intent_queue` frame; empty when no one's waiting. Pair with
386
+ * `claim_queue` frame; empty when no one's waiting. Pair with
387
387
  * `subscribe(...)` for change notifications.
388
388
  */
389
- queueFor(target: PresenceTarget): readonly Intent[];
389
+ queueFor(target: PresenceTarget): readonly Claim[];
390
390
  /**
391
391
  * Re-rank the wait queue on a target — move the listed waiters to the front
392
392
  * in the given order; unlisted waiters keep their relative FIFO order behind
393
- * them. Pass the `Intent[]` from `queueFor(target)` in the order you want
394
- * (each `Intent` carries its `heldBy` + `id`). Privileged: the server gates
395
- * it (a participant lacking the `intent.reorder` capability is denied), so
393
+ * them. Pass the `Claim[]` from `queueFor(target)` in the order you want
394
+ * (each `Claim` carries its `heldBy` + `id`). Privileged: the server gates
395
+ * it (a participant lacking the `claim.reorder` capability is denied), so
396
396
  * this is fire-and-forget — the new order arrives reactively via `queueFor`.
397
397
  */
398
- reorder(target: PresenceTarget, order: readonly Intent[]): void;
398
+ reorder(target: PresenceTarget, order: readonly Claim[]): void;
399
399
  /**
400
400
  * Framework-agnostic reactivity. Same contract as
401
401
  * `PresenceStream.subscribe` — register a listener fired on every
@@ -405,30 +405,30 @@ export interface IntentStream {
405
405
  */
406
406
  onChange(listener: () => void): () => void;
407
407
  /**
408
- * Observe server-side intent rejections. Fires when the server
409
- * rejects an `intents.writing(...)` / `announce(...)` call because
408
+ * Observe server-side claim rejections. Fires when the server
409
+ * rejects an `claims.writing(...)` / `announce(...)` call because
410
410
  * another participant already holds an open claim on the same
411
411
  * target (cooperative mutex → enforced at the server boundary).
412
412
  *
413
413
  * Use this to surface conflicts to the user:
414
414
  * ```ts
415
- * participant.intents.onRejected((r) => {
415
+ * participant.claims.onRejected((r) => {
416
416
  * toast.error(`${r.heldBy} is editing — try again in a moment`);
417
417
  * });
418
418
  * ```
419
419
  *
420
420
  * Returns an unsubscribe fn.
421
421
  */
422
- onRejected(listener: (rejection: IntentRejection) => void): () => void;
422
+ onRejected(listener: (rejection: ClaimRejection) => void): () => void;
423
423
  /**
424
- * Observe LOSING an intent you held — distinct from `onRejected` (a claim the
425
- * server refused). Fires on the server's `intent_lost` frame, carrying why:
424
+ * Observe LOSING an claim you held — distinct from `onRejected` (a claim the
425
+ * server refused). Fires on the server's `claim_lost` frame, carrying why:
426
426
  * `'preempted'` (a privileged participant evicted you) or `'expired'` (your
427
427
  * TTL lapsed). Lets a holder react — re-plan vs re-claim — instead of
428
428
  * silently discovering the lease gone via presence.
429
429
  *
430
430
  * ```ts
431
- * participant.intents.onLost((lost) => {
431
+ * participant.claims.onLost((lost) => {
432
432
  * if (lost.reason === 'preempted') replanAgainst(lost.target);
433
433
  * else reclaim(lost.target);
434
434
  * });
@@ -436,55 +436,29 @@ export interface IntentStream {
436
436
  *
437
437
  * Returns an unsubscribe fn.
438
438
  */
439
- onLost(listener: (lost: IntentLost) => void): () => void;
439
+ onLost(listener: (lost: ClaimLost) => void): () => void;
440
440
  /**
441
- * Async-iterable view of everyone else's open intents. Each
441
+ * Async-iterable view of everyone else's open claims. Each
442
442
  * iteration yields the current snapshot on every mutation.
443
443
  *
444
444
  * ```ts
445
- * for await (const openIntents of participant.intents) {
446
- * if (openIntents.some((i) => i.target.id === clauseId)) wait();
445
+ * for await (const openClaims of participant.claims) {
446
+ * if (openClaims.some((i) => i.target.id === clauseId)) wait();
447
447
  * }
448
448
  * ```
449
449
  */
450
- [Symbol.asyncIterator](): AsyncIterableIterator<ReadonlyArray<ActiveIntent>>;
450
+ [Symbol.asyncIterator](): AsyncIterableIterator<ReadonlyArray<ActiveClaim>>;
451
451
  }
452
452
  /**
453
- * Shape of an `intent_rejected` event delivered to
454
- * `IntentStream.onRejected`. Server rejects an incoming claim when
455
- * another participant already holds an open intent on the same target.
456
- */
457
- export interface IntentRejection {
458
- /** The rejected claim's id (the one the caller just tried to mint). */
459
- readonly intentId: string;
460
- /** Why the server rejected it — currently always `'conflict'`. */
461
- readonly reason: 'conflict';
462
- /** The target that's already held. */
463
- readonly target: {
464
- readonly entityType: string;
465
- readonly entityId: string;
466
- readonly path?: string;
467
- readonly range?: TargetRange;
468
- readonly field?: string;
469
- readonly meta?: Record<string, unknown>;
470
- };
471
- /** Participant id holding the existing claim. */
472
- readonly heldBy: string;
473
- /** The existing claim's id (for audit / retry correlation). */
474
- readonly heldByIntentId: string;
475
- /** When the existing claim expires (ms since epoch). */
476
- readonly heldByExpiresAt: number;
477
- }
478
- /**
479
- * You LOST an intent you were HOLDING — distinct from `IntentRejection` (a
453
+ * You LOST an claim you were HOLDING — distinct from `ClaimRejection` (a
480
454
  * claim the server refused you). Delivered via `onLost`.
481
455
  */
482
- export interface IntentLost {
456
+ export interface ClaimLost {
483
457
  /** The held claim's id that you just lost. */
484
- readonly intentId: string;
458
+ readonly claimId: string;
485
459
  /**
486
460
  * How you lost it. `'preempted'`: a privileged participant (one holding the
487
- * `intent.preempt` capability) evicted you and took the lease — its work now
461
+ * `claim.preempt` capability) evicted you and took the lease — its work now
488
462
  * supersedes yours, so re-plan against the new holder rather than blindly
489
463
  * re-claiming. `'expired'`: your TTL lapsed without finishing — re-claim if
490
464
  * you still need it.
@@ -500,7 +474,7 @@ export interface IntentLost {
500
474
  readonly meta?: Record<string, unknown>;
501
475
  };
502
476
  }
503
- export interface IntentDeclaration {
477
+ export interface ClaimDeclaration {
504
478
  readonly target: EntityRef;
505
479
  /** Human-readable reason — "rewriting title" / "restyling chart". */
506
480
  readonly reason: string;
@@ -517,58 +491,100 @@ export interface IntentDeclaration {
517
491
  *
518
492
  * ```ts
519
493
  * {
520
- * await using work = participant.intents.analyzing(clause, { ttl: '3m' });
521
- * // ... do the work; intent auto-revokes when the block exits
494
+ * await using work = participant.claims.analyzing(clause, { ttl: '3m' });
495
+ * // ... do the work; claim auto-revokes when the block exits
522
496
  * }
523
497
  * ```
524
498
  */
525
- export interface Claim extends AsyncDisposable {
526
- readonly id: string;
499
+ /**
500
+ * THE one claim handle. Returned by every claim door — the typed
501
+ * `ablo.<model>.claim({ id })` (rich: `data`/`readAt`/`target` populated) and
502
+ * the low-level `participant.claims.claim()` lease (minimal: `claimId` +
503
+ * `revoke`/`release`). Row-level fields are optional precisely because the
504
+ * low-level lease has no row snapshot; the model door fills them in.
505
+ *
506
+ * Implements `Symbol.asyncDispose` so callers can `await using claim = ...`
507
+ * and have it auto-release on scope exit.
508
+ */
509
+ export interface ClaimHandle<T = Record<string, unknown>> extends AsyncDisposable {
510
+ readonly object: 'claim';
511
+ readonly claimId: string;
512
+ /**
513
+ * True when the grant came AFTER waiting in the server's FIFO line
514
+ * (`claim_granted`) — the authoritative "the row may have changed under us"
515
+ * signal. Absent for an immediate grant or a non-queued lease.
516
+ */
517
+ readonly waited?: boolean;
518
+ /**
519
+ * Sync watermark of the held snapshot (`data` was read at this stamp). Writes
520
+ * carrying the handle use it as the `readAt` stale guard. Present for
521
+ * model-scoped claims; absent for low-level leases.
522
+ */
523
+ readonly readAt?: number;
524
+ readonly target: {
525
+ readonly model: string;
526
+ readonly id: string;
527
+ readonly field?: string;
528
+ readonly path?: string;
529
+ readonly range?: TargetRange;
530
+ readonly meta?: Record<string, unknown>;
531
+ };
532
+ readonly action: string;
533
+ readonly description?: string;
534
+ /** Row snapshot — populated by `ablo.<model>.claim`; absent on low-level leases. */
535
+ readonly data?: T;
536
+ release(): Promise<void>;
527
537
  revoke(): void;
528
538
  }
529
- export interface ActiveIntent extends IntentDeclaration {
539
+ export interface ActiveClaim extends ClaimDeclaration {
530
540
  readonly id: string;
531
541
  readonly heldBy: string;
532
542
  /**
533
- * Whether the holding participant is a human (session) or an agent.
534
- * First-class field so UIs can style "agent editing X" differently
535
- * from "user editing X" without string-parsing `heldBy`.
543
+ * Whether the holding participant is a user (session), an agent, or a
544
+ * system actor. First-class field so UIs can style "agent editing X"
545
+ * differently from "user editing X" without string-parsing `heldBy`.
546
+ * Canonical `'user' | 'agent' | 'system'` — the presence/claim stream
547
+ * derives the value from the boolean `isAgent` wire flag (so it produces
548
+ * only `'user'`/`'agent'`), but the type stays the full union it shares
549
+ * with the HTTP claim surface and lease store.
536
550
  */
537
- readonly participantKind: 'human' | 'agent';
551
+ readonly participantKind: ParticipantKind;
538
552
  readonly description?: string;
539
- readonly announcedAt: string;
540
- readonly expiresAt: string;
553
+ /** Epoch-ms the claim was announced. */
554
+ readonly announcedAt: number;
555
+ /** Epoch-ms the server auto-expires it. */
556
+ readonly expiresAt: number;
541
557
  }
542
558
  /**
543
- * Every lifecycle state of a coordination intent, in one enum.
559
+ * Every lifecycle state of a coordination claim, in one enum.
544
560
  * `active` = the current holder (the lock). `queued` = waiting in the FIFO
545
561
  * line behind the holder (carries `position`). The terminal states drop the
546
- * intent from the synced set.
562
+ * claim from the synced set.
547
563
  */
548
- export type IntentStatus = 'active' | 'queued' | 'committed' | 'expired' | 'canceled';
564
+ export type ClaimStatus = 'active' | 'queued' | 'committed' | 'expired' | 'canceled';
549
565
  /** Options for waiting on a target to become free. */
550
- export interface IntentWaitOptions {
566
+ export interface ClaimWaitOptions {
551
567
  readonly timeout?: number;
552
568
  readonly pollInterval?: number;
553
569
  readonly signal?: AbortSignal;
554
570
  }
555
571
  /**
556
572
  * The coordination state of one entity. Self-describing on the wire via
557
- * `object: 'intent'`. Existence with `status: 'active'` *is* the lock;
573
+ * `object: 'claim'`. Existence with `status: 'active'` *is* the lock;
558
574
  * the fields *are* the awareness ("agent X is editing this until Y").
559
575
  *
560
576
  * Deliberately omits a Stripe-style `next_action`: a contender's only
561
577
  * response is "wait until free, then re-read", and the runtime performs
562
578
  * that uniformly — `claim` serializes behind the holder via the server
563
- * FIFO queue (or low-level `intents.waitFor` to wait without claiming), and the
579
+ * FIFO queue (or low-level `claims.waitFor` to wait without claiming), and the
564
580
  * stale-context guard forces the re-read. Encoding a constant instruction
565
581
  * the engine always takes would be the kind of ceremony this object exists
566
582
  * to remove.
567
583
  */
568
- export interface Intent {
569
- readonly object: 'intent';
584
+ export interface Claim {
585
+ readonly object: 'claim';
570
586
  readonly id: string;
571
- readonly status: IntentStatus;
587
+ readonly status: ClaimStatus;
572
588
  /** What is being coordinated. */
573
589
  readonly target: EntityRef;
574
590
  /** Human-readable phase — `'editing'`, `'writing'`, `'reviewing'`. */
@@ -577,14 +593,14 @@ export interface Intent {
577
593
  readonly description?: string;
578
594
  /** Participant holding it. */
579
595
  readonly heldBy: string;
580
- readonly participantKind: 'human' | 'agent';
596
+ readonly participantKind: ParticipantKind;
581
597
  /**
582
- * Ms-epoch the holder opened it. Optional until the lease wire carries
598
+ * Epoch-ms the holder opened it. Optional until the lease wire carries
583
599
  * it — derived shapes (e.g. mapped from a presence frame) may omit it.
584
600
  */
585
- readonly createdAt?: string;
586
- /** Ms-epoch the server auto-expires it if the holder doesn't finish. */
587
- readonly expiresAt: string;
601
+ readonly createdAt?: number;
602
+ /** Epoch-ms the server auto-expires it if the holder doesn't finish. */
603
+ readonly expiresAt: number;
588
604
  /**
589
605
  * 0-based place in the FIFO line — present only when `status: 'queued'`
590
606
  * (`0` = next in line behind the holder). Absent for the active holder.
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Ablo treats humans and agents as participants on live application
5
5
  * entities. Participants announce what they are reading or editing,
6
- * claim intent before writing, and capture context watermarks before
6
+ * claim before writing, and capture context watermarks before
7
7
  * long-running AI work. The customer keeps their own schema, agent
8
8
  * stack, tools, prompts, and product policy; the sync engine provides
9
9
  * the shared coordination substrate.
@@ -5,7 +5,7 @@
5
5
  * The inputs are two functions:
6
6
  *
7
7
  * - `subscribe(onChange): unsubscribe` — the existing reactivity
8
- * primitive on `PresenceStream` / `IntentStream`. We register a
8
+ * primitive on `PresenceStream` / `ClaimStream`. We register a
9
9
  * listener that enqueues a value every time the source mutates;
10
10
  * we tear it down in `return()`.
11
11
  * - `getSnapshot()` — read the latest value to hand to the
@@ -5,7 +5,7 @@
5
5
  * The inputs are two functions:
6
6
  *
7
7
  * - `subscribe(onChange): unsubscribe` — the existing reactivity
8
- * primitive on `PresenceStream` / `IntentStream`. We register a
8
+ * primitive on `PresenceStream` / `ClaimStream`. We register a
9
9
  * listener that enqueues a value every time the source mutates;
10
10
  * we tear it down in `return()`.
11
11
  * - `getSnapshot()` — read the latest value to hand to the
@@ -52,7 +52,7 @@ export interface CommitOperation {
52
52
  }
53
53
  /**
54
54
  * Client → Server single named-mutation frame. The named-mutator write
55
- * primitive (intent + args), as opposed to the raw-op {@link CommitMessage}
55
+ * primitive (claim + args), as opposed to the raw-op {@link CommitMessage}
56
56
  * batch. Server-side mutator dispatch resolves `mutatorName` against the
57
57
  * host-provided registry.
58
58
  */
@@ -78,7 +78,7 @@ export interface CommitMessage {
78
78
  /**
79
79
  * Dormant agent-task lineage field. The SDK no longer populates it —
80
80
  * turns/tasks were removed and write attribution now rides on the
81
- * claim (`intent`) id plus the server-stamped actor/capability. Kept
81
+ * claim (`claim`) id plus the server-stamped actor/capability. Kept
82
82
  * optional for wire-compat; when present the Hub still validates and
83
83
  * threads it onto `caused_by_task_id`, but client writes leave it
84
84
  * `null` (the audit pane treats null as "no prompt-side context").
package/docs/api.md CHANGED
@@ -120,18 +120,18 @@ coordination surface is `claim.state({ id })` / `claim.queue({ id })` /
120
120
 
121
121
  | Field | Type | Description |
122
122
  |---|---|---|
123
- | `object` | `'intent'` | String representing the object's type. |
123
+ | `object` | `'claim'` | String representing the object's type. |
124
124
  | `id` | string | Unique identifier for the claim. |
125
125
  | `status` | `'active' \| 'queued' \| 'committed' \| 'expired' \| 'canceled'` | The whole lifecycle, in one field. `active` is the holder; `queued` is a waiter in the FIFO line behind it. |
126
126
  | `target` | `{ type, id, field? }` | What is being coordinated. |
127
127
  | `action` | string | Human-readable phase — `'editing'`, `'writing'`, `'reviewing'`. |
128
128
  | `heldBy` | string | Participant id holding the claim. |
129
- | `participantKind` | `'human' \| 'agent'` | Whether a human session or an agent holds it. |
129
+ | `participantKind` | `'user' \| 'agent' \| 'system'` | Who's behind it — a human (`user`), an AI (`agent`), or automated infrastructure (`system`). |
130
130
  | `expiresAt` | string | Ms-epoch at which the server auto-expires it if the holder doesn't finish. |
131
131
 
132
132
  ```json
133
133
  {
134
- "object": "intent",
134
+ "object": "claim",
135
135
  "id": "claim_3MtwBwLkdIwHu7ix",
136
136
  "status": "active",
137
137
  "target": { "type": "weatherReports", "id": "report_stockholm", "field": "status" },
@@ -29,6 +29,7 @@ Common options:
29
29
  |---|---|
30
30
  | `schema` | Required for typed model clients. |
31
31
  | `apiKey` | Bearer credential for trusted server runtimes. Defaults to `ABLO_API_KEY` when available. |
32
+ | `databaseUrl` | Optional, server-only. Registers your Postgres directly (the connection-string path). Pass it explicitly — it is **not** auto-read from the environment. Omit it for a signed Data Source endpoint or the hosted sandbox. The SDK throws if it sees this in a browser. |
32
33
  | `baseURL` | Override the hosted sync endpoint for staging or private deployments. |
33
34
  | `persistence` | `memory` by default. Use `indexeddb` for a durable browser cache that survives reloads. |
34
35
  | `fetch` | Custom fetch implementation for tests or non-standard runtimes. |
@@ -36,9 +37,11 @@ Common options:
36
37
  | `defaultQuery` | Extra query parameters attached to every HTTP request. |
37
38
  | `dangerouslyAllowBrowser` | Required before sending an API key from browser code. Prefer a server route instead. |
38
39
 
39
- There is intentionally no `databaseURL` constructor option. Teams that keep
40
- canonical rows in their own database use a signed [Data Source](./data-sources.md)
41
- endpoint.
40
+ `databaseUrl` is an optional, server-only constructor option. It is **not**
41
+ auto-read from the environment pass it explicitly to register your Postgres
42
+ directly (the connection-string path). Omit it when you expose a signed
43
+ [Data Source](./data-sources.md) endpoint, or when trying Ablo against the hosted
44
+ sandbox.
42
45
 
43
46
  ## Model Methods
44
47
 
@@ -29,7 +29,7 @@ make:
29
29
 
30
30
  | layer | kind | what it does | enforces? |
31
31
  |---|---|---|---|
32
- | **Presence** (`claim.state`, observers) | observation | Broadcasts who is working where, live. Renders cursors / "agent X is editing." | **No.** Advisory only — it never blocks or rejects a write. |
32
+ | **Presence** (`claim.state`, observers) | observation | Broadcasts who is working where, live. Renders cursors / "agent X is editing." Reading or claiming a row auto-enrolls you in its sync group, so `claim.state({ id })` observes co-participants from any client (browser or Node agent) with no manual subscribe step. | **No.** Advisory only — it never blocks or rejects a write. |
33
33
  | **Claim** (`claim`/`claim.queue`/`claim.release`) | pessimistic | Reserves a row for one participant. Foreign writers are rejected server-side; contenders join a fair FIFO queue. | **Yes**, between participants — mutual exclusion. |
34
34
  | **Stale-context** (`readAt` + `onStale`) | optimistic (LWW) | On commit, rejects a write whose snapshot is older than the row's latest delta. Last-writer-wins detection. | **Yes**, against time — lost-update detection. |
35
35
 
@@ -70,7 +70,7 @@ a model row. It's what `claim.state()` returns and what observers render.
70
70
  | `target` | `EntityRef` | What is being coordinated (`{ model, id, field? }`). |
71
71
  | `action` | `string` | Human-readable phase — `'editing'`, `'writing'`, `'reviewing'`. |
72
72
  | `heldBy` | `string` | Participant holding (or waiting on) it (e.g. `'agent:forecaster'`). |
73
- | `participantKind` | `'human' \| 'agent'` | Who's behind it. |
73
+ | `participantKind` | `'user' \| 'agent' \| 'system'` | Who's behind it — a human (`user`), an AI (`agent`), or automated infrastructure (`system`). |
74
74
  | `position` | `number?` | 0-based place in the FIFO line — present only when `status: 'queued'` (`0` = next behind the holder). |
75
75
  | `createdAt` | `string?` | Ms-epoch the holder opened it. Optional — derived shapes may omit it. |
76
76
  | `expiresAt` | `string` | Ms-epoch the server reclaims it if the holder goes **silent**. Renewed automatically while the holder's connection stays alive — a crash-cleanup floor, not a duration you size. |
@@ -167,6 +167,16 @@ ablo.<model>.claim.state({ id })
167
167
  Read who's currently working on a row, for observers and UI. Synchronous and
168
168
  reactive (it reads the local coordination snapshot). Never blocks.
169
169
 
170
+ **You don't subscribe to anything first.** Reading or claiming a row
171
+ automatically enrolls you in that row's sync group: reading it (including
172
+ `retrieve`/`get`, or `claim.state` itself) gives you **read-interest**, and
173
+ `claim`-ing it gives you a **pinned write-intent**. So `claim.state({ id })`
174
+ observes co-participants on that row from **any** client — a browser, a Server
175
+ Action, or a Node agent — and a holder sees its own claim, with no manual
176
+ subscribe step. There is no `participants.join` to call: the typed
177
+ `ablo.<model>` surface (read / `claim` / `claim.state` / `claim.queue`) is the
178
+ whole coordination API.
179
+
170
180
  **Parameters**
171
181
 
172
182
  | name | type | required | description |
@@ -306,7 +316,7 @@ inspect the `code`.
306
316
  | `AbloClaimedError` | `claim_conflict` | An `update`/`delete` targets a row another participant holds — the server's pre-commit check rejected it. | — |
307
317
  | `AbloClaimedError` | `entity_claimed` | Same conflict, from the commit guard backstop. | — |
308
318
  | `AbloStaleContextError` | — | A guarded `update` (under a claim, or any write carrying `readAt`) targets a row that received deltas since the snapshot — your reasoning is stale. | `readAt`, `conflicts[]` |
309
- | `AbloValidationError` | `model_claim_not_configured` | `claim` called on a model without collaboration wiring. | — |
319
+ | `AbloValidationError` | `model_claim_not_configured` | `claim` called on a model proxy built without the collaboration runtime — an internal/advanced construction path. The standard `Ablo({ schema, apiKey })` client enables claiming for **every** model; there is no per-model claim config to add. | — |
310
320
  | `AbloValidationError` | `entity_not_found` | The row id doesn't exist locally or on load. | — |
311
321
 
312
322
  `AbloStaleContextError.conflicts` lists the `(model, id, observedSyncId)` rows
@@ -1,15 +1,20 @@
1
1
  # Connect Your Database
2
2
 
3
- **Your database is the system of record Ablo never hosts your data.** Every
4
- synced model is backed by your own Postgres; Ablo is the transaction layer on
5
- top of it. There are two ways to connect, and they are the same product with the
6
- same writes — the only difference is where your database credential lives:
3
+ **In production, your database is the system of record.** Every synced model is
4
+ backed by your own Postgres; Ablo is the transaction layer on top of it. There
5
+ are two ways to connect, and they are the same product with the same writes — the
6
+ only difference is where your database credential lives:
7
7
 
8
8
  | | How Ablo reaches your Postgres | Use when |
9
9
  |---|---|---|
10
- | **Connection string** (default) | You pass `databaseUrl` to `Ablo(...)`; Ablo registers the connection and commits each write directly, behind row-level security. | You can hand over a scoped connection string. |
10
+ | **Connection string** (primary) | You pass `databaseUrl` to `Ablo(...)` explicitly (it is never auto-read from the environment); Ablo registers the connection and commits each write directly, behind row-level security. | You can hand over a scoped connection string. |
11
11
  | **Signed endpoint** | Your app exposes one route built from an ORM adapter; Ablo sends signed commit requests and your app writes its own database. | Database credentials must never leave your infrastructure. |
12
12
 
13
+ > Just trying Ablo? You don't need a database at all to start: the hosted
14
+ > **sandbox** can host rows in Ablo's test plane — pass an `apiKey` only and omit
15
+ > `databaseUrl`, like Stripe test mode. Connect your Postgres (either shape
16
+ > below) when you're ready for it to be the system of record.
17
+
13
18
  Either way, you define an Ablo schema with `defineSchema`, `model`, and Zod. The
14
19
  Ablo schema describes **only your synced, collaborative models** — the rows Ablo
15
20
  coordinates and fans out in realtime. It is *not* your whole-database schema and
@@ -36,7 +41,7 @@ import { schema } from './ablo/schema';
36
41
  export const ablo = Ablo({
37
42
  schema,
38
43
  apiKey: process.env.ABLO_API_KEY,
39
- databaseUrl: process.env.DATABASE_URL, // your Postgres — rows live here, never with Ablo
44
+ databaseUrl: process.env.DATABASE_URL, // your Postgres, passed explicitly — rows live here
40
45
  });
41
46
  ```
42
47
 
@@ -50,6 +55,20 @@ On first connect the SDK registers the connection — sent once over TLS, stored
50
55
  sealed, never returned by any API. From then on Ablo commits every confirmed
51
56
  write directly to your database and reads canonical rows from it.
52
57
 
58
+ ### A localhost Postgres can't be the system of record
59
+
60
+ This is the connection-string fact people hit first. Ablo's **cloud** registers
61
+ your connection string and connects to your Postgres **over the network**. A
62
+ `localhost` / private-range database (`127.0.0.1`, `192.168.*`, Docker's
63
+ `db:5432`) is unreachable from Ablo's side, so such connection strings are
64
+ **rejected**. Two escape hatches for local development against your own DB:
65
+
66
+ - **Expose a signed Data Source endpoint.** Your app — which *can* reach your
67
+ local DB — proxies Ablo's commits to it. See [Signed Endpoint](#signed-endpoint)
68
+ below. This is the right answer for "my dev DB stays on my machine."
69
+ - **Use the hosted sandbox.** Skip the database entirely: pass an `apiKey` only,
70
+ omit `databaseUrl`, and let Ablo's test plane host the rows while you build.
71
+
53
72
  Safety requirements, enforced server-side before the first write:
54
73
 
55
74
  - **Non-superuser role.** The connection must not be a superuser or hold
@@ -58,11 +77,12 @@ Safety requirements, enforced server-side before the first write:
58
77
  - **Row-level security on synced tables.** `npx ablo migrate` provisions your
59
78
  synced-model tables with `FORCE ROW LEVEL SECURITY` already applied; tables
60
79
  you create yourself must do the same.
61
- - **Public hosts only.** Connection strings resolving to loopback or private
62
- address ranges are rejected.
80
+ - **Network-reachable host.** As above, connection strings resolving to loopback
81
+ or private address ranges are rejected — Ablo connects from its cloud.
63
82
 
64
83
  `databaseUrl` is server-only: the SDK throws if it sees one in a browser-like
65
- environment, and `dangerouslyAllowBrowser` does not override that.
84
+ environment, and `dangerouslyAllowBrowser` does not override that. It is also
85
+ never auto-read from the environment — pass it explicitly to `Ablo(...)`.
66
86
 
67
87
  ## Signed Endpoint
68
88