@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
package/dist/Database.js CHANGED
@@ -20,6 +20,17 @@ export class Database {
20
20
  get helper() {
21
21
  return this.bootstrapHelper;
22
22
  }
23
+ /**
24
+ * PURE scoped snapshot fetch for hydrate-on-enter (P4). Returns the FULL
25
+ * current rows of the given sync groups, with NO side effects — unlike
26
+ * {@link bootstrapFromServer}, it does not persist to IndexedDB and does not
27
+ * touch the connection's `subscribedSyncGroups` (which the shrinkage check
28
+ * owns). The caller applies the result to the pool via the SCOPED apply path.
29
+ */
30
+ async fetchScopedBootstrapData(syncGroups) {
31
+ // No lastSyncId → a full snapshot of exactly these groups.
32
+ return this.bootstrapHelper.fetchBootstrap(undefined, syncGroups);
33
+ }
23
34
  // Database state
24
35
  currentDbInfo = null;
25
36
  workspaceDb = null;
@@ -764,7 +775,10 @@ export class Database {
764
775
  // ========================================================================
765
776
  // CONFLICT CHECK: Skip UPDATE/INSERT if DELETE exists with higher syncId
766
777
  // ========================================================================
767
- if (delta.actionType === 'U' || delta.actionType === 'I' || delta.actionType === 'C') {
778
+ if (delta.actionType === 'U' ||
779
+ delta.actionType === 'I' ||
780
+ delta.actionType === 'C' ||
781
+ delta.actionType === 'M') {
768
782
  const key = `${delta.modelName}:${delta.modelId}`;
769
783
  const deleteSyncId = deleteSyncIds.get(key);
770
784
  if (deleteSyncId !== undefined) {
@@ -503,7 +503,18 @@ export declare class SyncClient extends EventEmitter {
503
503
  applyBootstrapDataToPool(bootstrapData: {
504
504
  models?: Record<string, unknown[]>;
505
505
  failedModels?: string[];
506
- }, protectedIds?: ReadonlySet<string>): {
506
+ }, protectedIds?: ReadonlySet<string>, options?: {
507
+ /**
508
+ * SCOPED backfill (P4 hydrate-on-enter): the snapshot covers only the
509
+ * groups just entered, NOT the whole type. Two behaviors change to keep
510
+ * it from corrupting the pool:
511
+ * - upsert is version-guarded ({@link ObjectPool.upsertIfNewer}) so a
512
+ * concurrent live delta isn't clobbered back to the snapshot version;
513
+ * - ghost removal is SKIPPED — a subset snapshot must never evict rows
514
+ * of the same type that belong to other (unhydrated) groups.
515
+ */
516
+ scoped?: boolean;
517
+ }): {
507
518
  added: number;
508
519
  updated: number;
509
520
  removed: number;
@@ -7,6 +7,7 @@
7
7
  * - Send mutations to server via API client
8
8
  * - Handle conflict resolution for local changes
9
9
  */
10
+ import { runInAction } from 'mobx';
10
11
  import { ModelScope } from './ObjectPool.js';
11
12
  // ModelRegistry instance accessed via this.objectPool.registry
12
13
  import { LoadStrategy } from './types/index.js';
@@ -17,6 +18,31 @@ import { NetworkMonitor } from './NetworkMonitor.js';
17
18
  import { TransactionQueue } from './transactions/TransactionQueue.js';
18
19
  import { OptimisticEchoTracker, } from './transactions/OptimisticEchoTracker.js';
19
20
  import { SyncPosition } from './sync/syncPosition.js';
21
+ /**
22
+ * Is the raw snapshot record strictly newer than the pooled model? Compares
23
+ * server-stamped `updatedAt` (the engine has no numeric row version; the delta
24
+ * pipeline is arrival-ordered last-write-wins). An undefined incoming time is
25
+ * treated as NOT newer (don't clobber a known row); an undefined existing time
26
+ * means the pool row is unversioned, so the incoming record wins. Used by the
27
+ * scoped hydrate-on-enter apply to drop snapshot rows a live delta already
28
+ * advanced past the snapshot watermark.
29
+ */
30
+ function rawRecordIsNewer(data, existing) {
31
+ const raw = data.updatedAt;
32
+ const inMs = raw instanceof Date
33
+ ? raw.getTime()
34
+ : typeof raw === 'string'
35
+ ? (Number.isNaN(Date.parse(raw)) ? undefined : Date.parse(raw))
36
+ : typeof raw === 'number'
37
+ ? raw
38
+ : undefined;
39
+ const exMs = existing.updatedAt instanceof Date ? existing.updatedAt.getTime() : undefined;
40
+ if (inMs === undefined)
41
+ return false;
42
+ if (exMs === undefined)
43
+ return true;
44
+ return inMs > exMs;
45
+ }
20
46
  export class SyncClient extends EventEmitter {
21
47
  objectPool;
22
48
  database;
@@ -292,7 +318,10 @@ export class SyncClient extends EventEmitter {
292
318
  // server processes it, we drain on rollback too so a stale id
293
319
  // doesn't permanently silence a foreign delta sharing the same id
294
320
  // (vanishingly unlikely for UUIDs, but cheap insurance).
295
- this.transactionQueue.on('transaction:created', (tx) => this.echoTracker.markPending(tx.id));
321
+ this.transactionQueue.on('transaction:created', (tx) => {
322
+ if (!tx.localOnly)
323
+ this.echoTracker.markPending(tx.id);
324
+ });
296
325
  this.transactionQueue.on('optimistic:rollback', (event) => {
297
326
  this.echoTracker.drainOnRollback(event.transaction.id);
298
327
  });
@@ -654,6 +683,20 @@ export class SyncClient extends EventEmitter {
654
683
  * @see src/sync-engine/types/TrackableModel.ts for change capture pattern
655
684
  */
656
685
  mutate(type, model, poolAction, writeOptions) {
686
+ // No-op UPDATE guard (O(1)). An update with no dirty fields would travel
687
+ // to the server, get dropped by `coalesceOperations` Rule 4 (empty input),
688
+ // and — if it was the only op — come back as `lastSyncId: 0`. That trips
689
+ // `captureCommitZeroSyncId` (false-positive Sentry anomaly) AND parks the
690
+ // tx in `awaiting_delta` for a 30s reconciliation timeout on a write that
691
+ // changed nothing. `Model.hasChanges` reads `modifiedProperties.size`, so
692
+ // this costs O(1) with no allocation (vs. O(N) materializing getChanges()).
693
+ //
694
+ // Strict `=== false` is deliberate: `rowAsModel` only casts, so a non-Model
695
+ // object can reach here with `hasChanges === undefined`. `undefined === false`
696
+ // is false → we fall through to the normal path rather than risk dropping a
697
+ // real write. Only a genuine Model with an empty dirty-set is skipped.
698
+ if (type === 'update' && model.hasChanges === false)
699
+ return;
657
700
  // CRITICAL FIX: Capture changes BEFORE pool action
658
701
  // Pool operations (especially upsert) can clear _local changes
659
702
  // By capturing first, we ensure changes are never lost
@@ -720,6 +763,13 @@ export class SyncClient extends EventEmitter {
720
763
  const capturedChanges = changes && Object.keys(changes).length > 0
721
764
  ? Object.freeze({ ...changes })
722
765
  : this.captureModelChanges(model);
766
+ // No-op UPDATE guard: neither an explicit change set nor model dirty-fields.
767
+ // `captureModelChanges` already returns undefined for an empty dirty-set, so
768
+ // an undefined here means there is genuinely nothing to send — skip rather
769
+ // than emit an empty-input update that the server coalesces to lastSyncId 0
770
+ // (see the same guard in `mutate`).
771
+ if (capturedChanges === undefined)
772
+ return;
723
773
  this.objectPool.upsert(model, ModelScope.live);
724
774
  this.queueMutation({ type: 'update', model, timestamp: new Date(), capturedChanges });
725
775
  this.notifyObservers({
@@ -1552,25 +1602,39 @@ export class SyncClient extends EventEmitter {
1552
1602
  break;
1553
1603
  }
1554
1604
  }
1555
- // Batch ObjectPool mutations minimal MobX actions
1556
- if (modelsToAdd.length > 0)
1557
- this.objectPool.addBatch(modelsToAdd, ModelScope.live);
1558
- if (modelsToUpsert.length > 0)
1559
- this.objectPool.upsertBatch(modelsToUpsert, ModelScope.live);
1560
- if (idsToRemove.length > 0)
1561
- this.objectPool.removeBatch(idsToRemove);
1562
- for (const id of idsToArchive)
1563
- this.objectPool.updateScope(id, ModelScope.archived);
1564
- // Emit changed model types so QueryProcessor can auto-invalidate
1565
- const changedTypes = new Set(dbResults.map(r => r.modelName));
1566
- if (changedTypes.size > 0)
1567
- this.emit('models:changed', changedTypes);
1605
+ // Reveal the whole frame in ONE MobX action. `addBatch`/`upsertBatch`/
1606
+ // `removeBatch`/`updateScope` are each individually `action`-wrapped,
1607
+ // so calling them sequentially flushes reactions at every action
1608
+ // boundary a catch-up frame that adds + updates + removes would fire
1609
+ // every dependent reaction (the decks gallery, each open editor) 3-4×
1610
+ // in a row, re-rendering and re-sorting on each. Wrapping them in a
1611
+ // single outer `runInAction` defers all reaction flushes to ONE
1612
+ // boundary: dependents recompute exactly once regardless of how many
1613
+ // models or how many op-kinds the frame touched. This is the MobX
1614
+ // equivalent of Replicache's "atomically reveal the new state" — the
1615
+ // app never observes a partially-applied frame.
1616
+ runInAction(() => {
1617
+ if (modelsToAdd.length > 0)
1618
+ this.objectPool.addBatch(modelsToAdd, ModelScope.live);
1619
+ if (modelsToUpsert.length > 0)
1620
+ this.objectPool.upsertBatch(modelsToUpsert, ModelScope.live);
1621
+ if (idsToRemove.length > 0)
1622
+ this.objectPool.removeBatch(idsToRemove);
1623
+ for (const id of idsToArchive)
1624
+ this.objectPool.updateScope(id, ModelScope.archived);
1625
+ // Emit changed model types so QueryProcessor can auto-invalidate.
1626
+ // Kept inside the action so any observable query-cache state it
1627
+ // flips is part of the same atomic reveal.
1628
+ const changedTypes = new Set(dbResults.map(r => r.modelName));
1629
+ if (changedTypes.size > 0)
1630
+ this.emit('models:changed', changedTypes);
1631
+ });
1568
1632
  }
1569
1633
  /**
1570
1634
  * Apply bootstrap data to the ObjectPool with ghost removal.
1571
1635
  * Owns: model creation, batch upsert, ghost detection + removal.
1572
1636
  */
1573
- applyBootstrapDataToPool(bootstrapData, protectedIds) {
1637
+ applyBootstrapDataToPool(bootstrapData, protectedIds, options) {
1574
1638
  if (!bootstrapData.models) {
1575
1639
  return { added: 0, updated: 0, removed: 0, skipped: 0, healed: 0 };
1576
1640
  }
@@ -1605,6 +1669,19 @@ export class SyncClient extends EventEmitter {
1605
1669
  const recordId = data.id;
1606
1670
  if (recordId)
1607
1671
  idsForType.add(recordId);
1672
+ // Scoped backfill (P4 hydrate-on-enter): a subset snapshot is taken at
1673
+ // a server watermark. If a concurrent live delta already advanced this
1674
+ // row past the snapshot, skip it — `createFromData` mutates the pooled
1675
+ // model IN PLACE (the "keep instances alive" Linear pattern), so this
1676
+ // version guard MUST run BEFORE it; an upsert-layer guard would be too
1677
+ // late, the row would already be clobbered.
1678
+ if (options?.scoped && recordId) {
1679
+ const existing = this.objectPool.get(recordId);
1680
+ if (existing && !rawRecordIsNewer(data, existing)) {
1681
+ skippedCount++;
1682
+ continue;
1683
+ }
1684
+ }
1608
1685
  try {
1609
1686
  const model = this.objectPool.createFromData(data);
1610
1687
  if (model)
@@ -1615,23 +1692,30 @@ export class SyncClient extends EventEmitter {
1615
1692
  }
1616
1693
  }
1617
1694
  }
1618
- // Batch upsert
1695
+ // Upsert. The scoped stale-skip above already guarded the version, so a
1696
+ // plain upsert is correct here for both paths.
1619
1697
  const beforeSize = this.objectPool.size;
1620
1698
  this.objectPool.upsertBatch(allModels, ModelScope.live);
1621
1699
  const addedCount = this.objectPool.size - beforeSize;
1622
1700
  const updatedCount = allModels.length - addedCount;
1623
- // Ghost removal — remove pool entities not in server snapshot
1624
- const ghostIds = [];
1625
- for (const [modelType, serverIds] of serverIdsByType) {
1626
- const poolIds = this.objectPool.getIdsByModelType(modelType);
1627
- if (!poolIds)
1628
- continue;
1629
- for (const poolId of poolIds) {
1630
- if (!serverIds.has(poolId) && !protectedIds?.has(poolId))
1631
- ghostIds.push(poolId);
1701
+ // Ghost removal — remove pool entities not in the server snapshot. Only
1702
+ // valid for a FULL bootstrap, where the snapshot is authoritative for each
1703
+ // returned type. A SCOPED subset snapshot must NOT remove rows of the same
1704
+ // type that belong to other (unhydrated) groups.
1705
+ let removedCount = 0;
1706
+ if (!options?.scoped) {
1707
+ const ghostIds = [];
1708
+ for (const [modelType, serverIds] of serverIdsByType) {
1709
+ const poolIds = this.objectPool.getIdsByModelType(modelType);
1710
+ if (!poolIds)
1711
+ continue;
1712
+ for (const poolId of poolIds) {
1713
+ if (!serverIds.has(poolId) && !protectedIds?.has(poolId))
1714
+ ghostIds.push(poolId);
1715
+ }
1632
1716
  }
1717
+ removedCount = this.objectPool.removeBatch(ghostIds);
1633
1718
  }
1634
- const removedCount = this.objectPool.removeBatch(ghostIds);
1635
1719
  // Emit changed model types so QueryProcessor can auto-invalidate
1636
1720
  const changedTypes = new Set(Object.keys(bootstrapData.models));
1637
1721
  if (changedTypes.size > 0)
@@ -40,10 +40,10 @@
40
40
  * for custom integrations outside the AI SDK.
41
41
  */
42
42
  import type { PresenceAnnouncer, AgentContext } from './types.js';
43
- import type { Activity, IntentClaim } from '../types/streams.js';
43
+ import type { Activity, WireClaim } from '../types/streams.js';
44
44
  import { createAgentSession } from './session.js';
45
45
  export type { AgentContext } from './types.js';
46
- export type { IntentClaim } from '../types/streams.js';
46
+ export type { WireClaim } from '../types/streams.js';
47
47
  /**
48
48
  * Shape returned by the sync server's REST `/api/presence` endpoint.
49
49
  *
@@ -62,7 +62,7 @@ interface WirePeer {
62
62
  activity?: Activity;
63
63
  updatedAt?: number;
64
64
  organizationId?: string;
65
- activeIntents?: IntentClaim[];
65
+ activeClaims?: WireClaim[];
66
66
  }
67
67
  import type { SyncLogger } from '../interfaces/index.js';
68
68
  export interface AgentOptions {
@@ -125,13 +125,13 @@ export interface FreshnessCheck {
125
125
  /** Human-readable summary — feed this back to the LLM when stale. */
126
126
  summary?: string;
127
127
  /**
128
- * Pending-mutation intents from OTHER participants targeting this
129
- * entity (self-intents filtered out). Empty = no one else is
128
+ * Pending-mutation claims from OTHER participants targeting this
129
+ * entity (self-claims filtered out). Empty = no one else is
130
130
  * currently generating against this entity. Non-empty is ADVISORY
131
131
  * — the agent can proceed, wait, or defer. Stale-read protection
132
132
  * that predates committed deltas.
133
133
  */
134
- pendingIntents?: IntentClaim[];
134
+ pendingClaims?: WireClaim[];
135
135
  }
136
136
  /** Subset of AI SDK's ModelMessage — structural. */
137
137
  export interface AgentMessage {
@@ -299,13 +299,13 @@ export declare class Agent implements PresenceAnnouncer {
299
299
  */
300
300
  checkFreshness(entityType: string, entityId: string, lastSeenAt: number): Promise<FreshnessCheck>;
301
301
  /**
302
- * Pull the org's presence, filter to intents targeting the given
303
- * entity (self-intents excluded). Advisory — returns empty on any
302
+ * Pull the org's presence, filter to claims targeting the given
303
+ * entity (self-claims excluded). Advisory — returns empty on any
304
304
  * error so `checkFreshness` stays usable when the presence endpoint
305
305
  * is down. Case-insensitive match on entityType + entityId to absorb
306
306
  * PascalCase / lowercase divergence.
307
307
  */
308
- private fetchPendingIntentsFor;
308
+ private fetchPendingClaimsFor;
309
309
  /**
310
310
  * Build a `prepareStep` hook for AI SDK's generateText / streamText /
311
311
  * ToolLoopAgent. Called before each step — injects a system message
@@ -201,14 +201,14 @@ export class Agent {
201
201
  */
202
202
  async checkFreshness(entityType, entityId, lastSeenAt) {
203
203
  // Parallel fan-out: freshness (entity state vs lastSeenAt) + pending
204
- // intents (other agents about to mutate). Both are advisory — if
204
+ // claims (other agents about to mutate). Both are advisory — if
205
205
  // either request fails the check still returns a usable result.
206
- const [queryRes, pendingIntents] = await Promise.all([
206
+ const [queryRes, pendingClaims] = await Promise.all([
207
207
  this.request('POST', '/api/sync/query', {
208
208
  organizationId: this.opts.organizationId,
209
209
  queries: [{ model: entityType, ids: [entityId] }],
210
210
  }).catch((err) => ({ ok: false, status: 0, _err: err })),
211
- this.fetchPendingIntentsFor(entityType, entityId),
211
+ this.fetchPendingClaimsFor(entityType, entityId),
212
212
  ]);
213
213
  try {
214
214
  const res = queryRes;
@@ -217,7 +217,7 @@ export class Agent {
217
217
  stale: false,
218
218
  reason: 'ok',
219
219
  summary: `Freshness check inconclusive: ${('status' in res ? res.status : 'error')}`,
220
- pendingIntents,
220
+ pendingClaims,
221
221
  };
222
222
  }
223
223
  const body = (await res.json());
@@ -227,7 +227,7 @@ export class Agent {
227
227
  stale: true,
228
228
  reason: 'not_found',
229
229
  summary: `${entityType} ${entityId} no longer exists. Another actor may have deleted it.`,
230
- pendingIntents,
230
+ pendingClaims,
231
231
  };
232
232
  }
233
233
  const entity = rows[0];
@@ -251,7 +251,7 @@ export class Agent {
251
251
  summary: `${entityType} ${entityId} was modified by ${lastModifiedBy ?? 'another actor'} ` +
252
252
  `${ago}s ago. Your planned change is based on stale state. ` +
253
253
  `Re-read the entity and adjust your approach.`,
254
- pendingIntents,
254
+ pendingClaims,
255
255
  };
256
256
  }
257
257
  return {
@@ -260,7 +260,7 @@ export class Agent {
260
260
  currentState: entity,
261
261
  lastModifiedBy,
262
262
  lastModifiedAt,
263
- pendingIntents,
263
+ pendingClaims,
264
264
  };
265
265
  }
266
266
  catch (err) {
@@ -270,29 +270,29 @@ export class Agent {
270
270
  stale: false,
271
271
  reason: 'ok',
272
272
  summary: `Freshness check error: ${err.message}`,
273
- pendingIntents,
273
+ pendingClaims,
274
274
  };
275
275
  }
276
276
  }
277
277
  /**
278
- * Pull the org's presence, filter to intents targeting the given
279
- * entity (self-intents excluded). Advisory — returns empty on any
278
+ * Pull the org's presence, filter to claims targeting the given
279
+ * entity (self-claims excluded). Advisory — returns empty on any
280
280
  * error so `checkFreshness` stays usable when the presence endpoint
281
281
  * is down. Case-insensitive match on entityType + entityId to absorb
282
282
  * PascalCase / lowercase divergence.
283
283
  */
284
- async fetchPendingIntentsFor(entityType, entityId) {
284
+ async fetchPendingClaimsFor(entityType, entityId) {
285
285
  const etLower = entityType.toLowerCase();
286
286
  const idLower = entityId.toLowerCase();
287
287
  const entries = await this.fetchPresence(true);
288
288
  const result = [];
289
289
  for (const entry of entries) {
290
- if (!entry.activeIntents)
290
+ if (!entry.activeClaims)
291
291
  continue;
292
- for (const intent of entry.activeIntents) {
293
- if (intent.entityType.toLowerCase() === etLower &&
294
- intent.entityId.toLowerCase() === idLower) {
295
- result.push(intent);
292
+ for (const claim of entry.activeClaims) {
293
+ if (claim.entityType.toLowerCase() === etLower &&
294
+ claim.entityId.toLowerCase() === idLower) {
295
+ result.push(claim);
296
296
  }
297
297
  }
298
298
  }
@@ -8,7 +8,7 @@
8
8
  * ─────────────────────────────────────────────────────────────────────────
9
9
  * Use the unified `Ablo({...})` factory directly with `kind: 'agent'`.
10
10
  * The factory holds the WebSocket, reactive subscriptions, mutations, and
11
- * presence/intents — same surface as a browser user, just with a
11
+ * presence/claims — same surface as a browser user, just with a
12
12
  * server-issued capability token instead of session cookies.
13
13
  *
14
14
  * ```ts
@@ -8,7 +8,7 @@
8
8
  * ─────────────────────────────────────────────────────────────────────────
9
9
  * Use the unified `Ablo({...})` factory directly with `kind: 'agent'`.
10
10
  * The factory holds the WebSocket, reactive subscriptions, mutations, and
11
- * presence/intents — same surface as a browser user, just with a
11
+ * presence/claims — same surface as a browser user, just with a
12
12
  * server-issued capability token instead of session cookies.
13
13
  *
14
14
  * ```ts
@@ -122,7 +122,7 @@
122
122
  // const ctx: Agent.Context = { perception };
123
123
  // const s: Agent.SessionOptions = { ... };
124
124
  //
125
- // Everything else (Activity, Claim, Peer, ActiveIntent, ...)
125
+ // Everything else (Activity, Claim, Peer, ActiveClaim, ...)
126
126
  // lives on the `Ablo.*` namespace via
127
127
  // `import type { Ablo } from '@abloatai/ablo'`.
128
128
  export { Agent } from './Agent.js';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Agent-SDK abstractions. The engine's data vocabulary
3
- * (`Peer`, `Activity`, `IntentClaim`, `ActiveIntent`,
3
+ * (`Peer`, `Activity`, `Claim`, `ActiveClaim`,
4
4
  * `PresenceUpdatePayload`, `PresenceKind`) lives in
5
5
  * `../types/streams.ts`. This file holds only the bits that are
6
6
  * specific to the agent module: the `PresenceAnnouncer` abstraction
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Agent-SDK abstractions. The engine's data vocabulary
3
- * (`Peer`, `Activity`, `IntentClaim`, `ActiveIntent`,
3
+ * (`Peer`, `Activity`, `Claim`, `ActiveClaim`,
4
4
  * `PresenceUpdatePayload`, `PresenceKind`) lives in
5
5
  * `../types/streams.ts`. This file holds only the bits that are
6
6
  * specific to the agent module: the `PresenceAnnouncer` abstraction
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Intent broadcast middleware — wraps a language model so the agent
2
+ * Claim broadcast middleware — wraps a language model so the agent
3
3
  * declares "I'm about to edit entity X" over the sync engine's
4
- * intent primitive at stream start, and abandons the claim at
4
+ * claim primitive at stream start, and abandons the claim at
5
5
  * stream end.
6
6
  *
7
7
  * Cross-cutting by design — composes via the AI SDK's
@@ -13,20 +13,20 @@
13
13
  * the package's own `SyncAgent`. No app-specific assumptions —
14
14
  * Ablo's web app uses this, but so can any consumer of `@abloatai/ablo`.
15
15
  *
16
- * Cost: one WS frame at stream start (`intent_begin`), one at end
17
- * (`intent_abandon`). No DB I/O, no extra LLM tokens.
16
+ * Cost: one WS frame at stream start (`claim_begin`), one at end
17
+ * (`claim_abandon`). No DB I/O, no extra LLM tokens.
18
18
  */
19
19
  import type { LanguageModelV3Middleware } from '@ai-sdk/provider';
20
20
  import type { Ablo } from '../client/Ablo.js';
21
21
  import type { SchemaRecord } from '../schema/schema.js';
22
22
  /**
23
- * Target entity for the intent broadcast.
23
+ * Target entity for the claim broadcast.
24
24
  *
25
25
  * `entityType` is a free-form string — convention is the schema's
26
26
  * typename (e.g. `'SlideDeck'`, `'Task'`, `'Matter'`) so peers can
27
27
  * filter consistently. The wire format treats it opaquely.
28
28
  */
29
- export interface IntentTarget {
29
+ export interface ClaimTarget {
30
30
  readonly entityType: string;
31
31
  readonly entityId: string;
32
32
  /** Optional path for file/document-like targets. */
@@ -52,11 +52,11 @@ export interface IntentTarget {
52
52
  */
53
53
  readonly estimatedMs?: number;
54
54
  }
55
- export interface IntentBroadcastMiddlewareOptions<R extends SchemaRecord = SchemaRecord> {
55
+ export interface ClaimBroadcastMiddlewareOptions<R extends SchemaRecord = SchemaRecord> {
56
56
  /** Connected Ablo. Null disables the middleware (no-op). */
57
57
  readonly agent: Ablo<R> | null;
58
58
  /** Target entity. Null skips the broadcast (purely conversational). */
59
- readonly target: IntentTarget | null;
59
+ readonly target: ClaimTarget | null;
60
60
  /**
61
61
  * Action verb describing what the agent is doing. Convention:
62
62
  * `'edit'`, `'read'`, `'review'`, `'generate'`. Default `'edit'`.
@@ -64,7 +64,7 @@ export interface IntentBroadcastMiddlewareOptions<R extends SchemaRecord = Schem
64
64
  readonly action?: string;
65
65
  /**
66
66
  * Peer-visible explanation of the specific work this model call is about to
67
- * perform. Surfaces to other agents through `ActiveIntent.description`.
67
+ * perform. Surfaces to other agents through `ActiveClaim.description`.
68
68
  */
69
69
  readonly description?: string;
70
70
  }
@@ -79,4 +79,4 @@ export interface IntentBroadcastMiddlewareOptions<R extends SchemaRecord = Schem
79
79
  * widened version collapses model proxies to an index signature
80
80
  * that clashes with the named methods (`ready`, `dispose`, etc.).
81
81
  */
82
- export declare function intentBroadcastMiddleware<R extends SchemaRecord = SchemaRecord>(options: IntentBroadcastMiddlewareOptions<R>): LanguageModelV3Middleware;
82
+ export declare function claimBroadcastMiddleware<R extends SchemaRecord = SchemaRecord>(options: ClaimBroadcastMiddlewareOptions<R>): LanguageModelV3Middleware;
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Intent broadcast middleware — wraps a language model so the agent
2
+ * Claim broadcast middleware — wraps a language model so the agent
3
3
  * declares "I'm about to edit entity X" over the sync engine's
4
- * intent primitive at stream start, and abandons the claim at
4
+ * claim primitive at stream start, and abandons the claim at
5
5
  * stream end.
6
6
  *
7
7
  * Cross-cutting by design — composes via the AI SDK's
@@ -13,8 +13,8 @@
13
13
  * the package's own `SyncAgent`. No app-specific assumptions —
14
14
  * Ablo's web app uses this, but so can any consumer of `@abloatai/ablo`.
15
15
  *
16
- * Cost: one WS frame at stream start (`intent_begin`), one at end
17
- * (`intent_abandon`). No DB I/O, no extra LLM tokens.
16
+ * Cost: one WS frame at stream start (`claim_begin`), one at end
17
+ * (`claim_abandon`). No DB I/O, no extra LLM tokens.
18
18
  */
19
19
  /**
20
20
  * Build the middleware. When `agent` or `target` is null, returns a
@@ -27,14 +27,14 @@
27
27
  * widened version collapses model proxies to an index signature
28
28
  * that clashes with the named methods (`ready`, `dispose`, etc.).
29
29
  */
30
- export function intentBroadcastMiddleware(options) {
30
+ export function claimBroadcastMiddleware(options) {
31
31
  const { agent, target } = options;
32
32
  const action = options.action ?? 'edit';
33
33
  const description = options.description;
34
34
  const openClaim = () => {
35
35
  if (!agent || !target)
36
36
  return null;
37
- return agent.intents.claim({
37
+ return agent.claims.claim({
38
38
  type: target.entityType,
39
39
  id: target.entityId,
40
40
  path: target.path,
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Coordination context middleware — reads peer intents on the same
2
+ * Coordination context middleware — reads peer claims on the same
3
3
  * entity from the sync engine's presence stream and injects a brief
4
4
  * coordination note into the prompt before the LLM call.
5
5
  *
6
- * The complement of `intent-broadcast.ts`: that one declares what
6
+ * The complement of `claim-broadcast.ts`: that one declares what
7
7
  * THIS agent is about to do; this one reads what OTHERS are doing
8
8
  * and tells the LLM about it. Together they make multiplayer-with-
9
9
  * AI structurally real — the AI knows when a human or another
@@ -23,28 +23,28 @@
23
23
  import type { LanguageModelV3Middleware } from '@ai-sdk/provider';
24
24
  import type { Ablo } from '../client/Ablo.js';
25
25
  import type { SchemaRecord } from '../schema/schema.js';
26
- import type { IntentTarget } from './intent-broadcast.js';
26
+ import type { ClaimTarget } from './claim-broadcast.js';
27
27
  export interface CoordinationContextMiddlewareOptions<R extends SchemaRecord = SchemaRecord> {
28
28
  readonly agent: Ablo<R> | null;
29
- readonly target: IntentTarget | null;
29
+ readonly target: ClaimTarget | null;
30
30
  /**
31
- * Optional intentId(s) to exclude from the read — typically this
31
+ * Optional claimId(s) to exclude from the read — typically this
32
32
  * agent's own active claim so the coordination note doesn't tell
33
33
  * the AI "you yourself are editing this." When middleware is
34
- * composed with `intentBroadcastMiddleware` in the standard order,
34
+ * composed with `claimBroadcastMiddleware` in the standard order,
35
35
  * `transformParams` runs BEFORE the broadcast's `wrapStream`
36
36
  * declares its claim, so the agent's own claim isn't yet in the
37
37
  * cached presence and self-filtering isn't needed. The hook is
38
38
  * here for callers that compose differently or for fleet
39
- * coordination (filter sibling worker intents).
39
+ * coordination (filter sibling worker claims).
40
40
  */
41
- readonly excludeIntentIds?: readonly string[];
41
+ readonly excludeClaimIds?: readonly string[];
42
42
  }
43
43
  /**
44
44
  * Build the middleware. When `agent` or `target` is null, returns a
45
45
  * pass-through.
46
46
  *
47
- * Generic over the schema record — see `intentBroadcastMiddleware`
47
+ * Generic over the schema record — see `claimBroadcastMiddleware`
48
48
  * for why `Ablo<S>` and `Ablo<SchemaRecord>` aren't structurally
49
49
  * assignable.
50
50
  */
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Coordination context middleware — reads peer intents on the same
2
+ * Coordination context middleware — reads peer claims on the same
3
3
  * entity from the sync engine's presence stream and injects a brief
4
4
  * coordination note into the prompt before the LLM call.
5
5
  *
6
- * The complement of `intent-broadcast.ts`: that one declares what
6
+ * The complement of `claim-broadcast.ts`: that one declares what
7
7
  * THIS agent is about to do; this one reads what OTHERS are doing
8
8
  * and tells the LLM about it. Together they make multiplayer-with-
9
9
  * AI structurally real — the AI knows when a human or another
@@ -24,24 +24,24 @@
24
24
  * Build the middleware. When `agent` or `target` is null, returns a
25
25
  * pass-through.
26
26
  *
27
- * Generic over the schema record — see `intentBroadcastMiddleware`
27
+ * Generic over the schema record — see `claimBroadcastMiddleware`
28
28
  * for why `Ablo<S>` and `Ablo<SchemaRecord>` aren't structurally
29
29
  * assignable.
30
30
  */
31
31
  export function coordinationContextMiddleware(options) {
32
32
  const { agent, target } = options;
33
- const excludeIntentIds = new Set(options.excludeIntentIds ?? []);
33
+ const excludeClaimIds = new Set(options.excludeClaimIds ?? []);
34
34
  return {
35
35
  specificationVersion: 'v3',
36
36
  transformParams: async ({ params }) => {
37
37
  if (!agent || !target)
38
38
  return params;
39
- // Read peer intents on the same target. Synchronous lookup
40
- // against the engine's reactive intents.others array — no I/O.
41
- const peerClaims = agent.intents.others.filter((claim) => claim.target.type === target.entityType &&
39
+ // Read peer claims on the same target. Synchronous lookup
40
+ // against the engine's reactive claims.others array — no I/O.
41
+ const peerClaims = agent.claims.others.filter((claim) => claim.target.type === target.entityType &&
42
42
  claim.target.id === target.entityId &&
43
43
  targetsOverlap(claim.target, target) &&
44
- !excludeIntentIds.has(claim.id));
44
+ !excludeClaimIds.has(claim.id));
45
45
  if (peerClaims.length === 0)
46
46
  return params;
47
47
  const note = formatCoordinationNote(peerClaims, target);