@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
@@ -19,7 +19,9 @@
19
19
  * await sync.reports.delete({ id: reportId });
20
20
  */
21
21
  import { z } from 'zod';
22
- import { AbloClaimedError, AbloError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, translateHttpError, toAbloError } from '../errors.js';
22
+ import { baseFieldsSchema } from '../schema/schema.js';
23
+ import { AbloError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, translateHttpError, toAbloError, claimedError } from '../errors.js';
24
+ import { descriptionFromMeta } from '../coordination/schema.js';
23
25
  import { LoadStrategy, PropertyType } from '../types/index.js';
24
26
  import { initSyncEngine } from '../context.js';
25
27
  import { noopObservability, browserOnlineStatus, defaultSessionErrorDetector, noopAnalytics, } from '../SyncEngineContext.js';
@@ -32,15 +34,15 @@ import { resolveParticipantIdentity } from './identity.js';
32
34
  import { Model } from '../Model.js';
33
35
  import { BaseSyncedStore } from '../BaseSyncedStore.js';
34
36
  import { createPresenceStream } from '../sync/createPresenceStream.js';
35
- import { createIntentStream } from '../sync/createIntentStream.js';
36
- import { awaitIntentGrant } from '../sync/awaitIntentGrant.js';
37
+ import { createClaimStream } from '../sync/createClaimStream.js';
38
+ import { awaitClaimGrant } from '../sync/awaitClaimGrant.js';
37
39
  import { createSnapshot } from '../sync/createSnapshot.js';
38
40
  import { createParticipantManager } from '../sync/participants.js';
39
41
  import { createProtocolClient, } from './ApiClient.js';
40
42
  // Value import is cycle-safe: httpClient.js only value-imports ApiClient.js,
41
43
  // which imports this module type-only.
42
44
  import { createAbloHttpClient, } from './httpClient.js';
43
- import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, } from './auth.js';
45
+ import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, warnIfDatabaseUrlEnvIgnored, } from './auth.js';
44
46
  import { registerDataSource } from './registerDataSource.js';
45
47
  import { shouldUseInMemoryPersistence, } from './persistence.js';
46
48
  import { createModelProxy } from './createModelProxy.js';
@@ -240,7 +242,18 @@ function registerModelsFromSchema(schema, registry) {
240
242
  }
241
243
  // Create a dynamic Model subclass with JSON sub-property getters
242
244
  const isLazy = modelDef.lazyObservable === true;
243
- const fieldNames = Object.keys(modelDef.shape);
245
+ // Base provenance fields (`organizationId`, `createdBy`) live in
246
+ // `baseFieldsSchema`, not the per-model `shape`. The server stamps + emits
247
+ // them (camelCased on the wire), but hydration (`Model.assignFieldsFromData`)
248
+ // only assigns keys that already exist as an own/prototype property — so
249
+ // without a slot here, `deck.createdBy` / `deck.organizationId` silently read
250
+ // `undefined` (this is why the profile decks tab showed nothing: it filters
251
+ // `decks.filter(d => d.createdBy === userId)`). `id`/`createdAt`/`updatedAt`
252
+ // are already seeded by the base Model constructor, so they're excluded.
253
+ const fieldNames = [
254
+ ...Object.keys(modelDef.shape),
255
+ ...Object.keys(baseFieldsSchema.shape).filter((f) => f !== 'id' && f !== 'createdAt' && f !== 'updatedAt' && !(f in modelDef.shape)),
256
+ ];
244
257
  const computed = modelDef.computed;
245
258
  const DynamicModel = createDynamicModelClass(modelName, jsonSubFields, fieldNames, computed, isLazy);
246
259
  // Respect the schema's load strategy so lazy models skip IDB hydration + bootstrap
@@ -712,6 +725,9 @@ export function Ablo(options) {
712
725
  dangerouslyAllowBrowser: options.dangerouslyAllowBrowser,
713
726
  });
714
727
  const { logger = consoleLogger } = internalOptions;
728
+ // Nudge (once) if a stray DATABASE_URL is in the env but `databaseUrl` wasn't
729
+ // passed — the env value is no longer auto-adopted (see resolveDatabaseUrl).
730
+ warnIfDatabaseUrlEnvIgnored(authInput, (m) => logger.warn(m));
715
731
  const schema = options.schema;
716
732
  const url = resolveBaseURL(authInput);
717
733
  // 1. Derive config from schema
@@ -821,8 +837,8 @@ export function Ablo(options) {
821
837
  // becomes null, so the first Ablo's commits start throwing
822
838
  // `ws_not_ready` forever (terminal AgentJob writes hang on retry).
823
839
  syncClient.getTransactionQueue().setMutationExecutor(executor);
824
- // Presence + intent streams — built eagerly so `engine.presence`
825
- // and `engine.intents` return the same reference for the engine's
840
+ // Presence + claim streams — built eagerly so `engine.presence`
841
+ // and `engine.claims` return the same reference for the engine's
826
842
  // lifetime. The transport doesn't exist yet (BaseSyncedStore.initialize
827
843
  // creates it during ready()), so both streams are constructed in
828
844
  // deferred-attach mode and wired after initialize() resolves below.
@@ -837,12 +853,12 @@ export function Ablo(options) {
837
853
  syncGroups: internalOptions.syncGroups ?? [],
838
854
  isAgent: internalOptions.kind === 'agent',
839
855
  });
840
- const intentStream = createIntentStream({ participantId });
856
+ const claimStream = createClaimStream({ participantId });
841
857
  const participantManager = createParticipantManager({
842
858
  ready,
843
859
  getTransport: () => store.getSyncWebSocket() ?? null,
844
860
  presence: presenceStream,
845
- intents: intentStream,
861
+ claims: claimStream,
846
862
  schema,
847
863
  });
848
864
  // 6. Validate options up front — fail loudly on obviously wrong inputs so
@@ -990,14 +1006,14 @@ export function Ablo(options) {
990
1006
  code: 'bootstrap_fetch_timeout',
991
1007
  });
992
1008
  }
993
- // Wire presence + intents to the now-open transport.
1009
+ // Wire presence + claims to the now-open transport.
994
1010
  // `getSyncWebSocket()` returns non-null after a successful
995
1011
  // initialize() — the WS is created during the generator's
996
1012
  // connect step.
997
1013
  const ws = store.getSyncWebSocket();
998
1014
  if (ws) {
999
1015
  presenceStream.attach(ws);
1000
- intentStream.attach(ws);
1016
+ claimStream.attach(ws);
1001
1017
  }
1002
1018
  logger.info('Sync engine ready', { models: Object.keys(schema.models).length });
1003
1019
  }
@@ -1073,10 +1089,10 @@ export function Ablo(options) {
1073
1089
  ? crypto.randomUUID()
1074
1090
  : `id_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
1075
1091
  }
1076
- function normalizeIntentId(intent) {
1077
- if (typeof intent === 'string')
1078
- return intent;
1079
- return intent?.id;
1092
+ function normalizeClaimId(claim) {
1093
+ if (typeof claim === 'string')
1094
+ return claim;
1095
+ return claim?.id;
1080
1096
  }
1081
1097
  function isClaimHandleValue(value) {
1082
1098
  return (typeof value === 'object' &&
@@ -1113,82 +1129,72 @@ export function Ablo(options) {
1113
1129
  }
1114
1130
  return inputOperations.map((op) => normalizeCommitOperation(op, commitOptions));
1115
1131
  }
1116
- function modelClaimFromActive(intent) {
1117
- const description = typeof intent.target.meta?.description === 'string'
1118
- ? intent.target.meta.description
1119
- : undefined;
1132
+ function modelClaimFromActive(claim) {
1133
+ const description = descriptionFromMeta(claim.target.meta);
1120
1134
  return {
1121
- id: intent.id,
1122
- actor: intent.heldBy,
1123
- participantKind: intent.participantKind,
1124
- action: intent.reason,
1135
+ id: claim.id,
1136
+ actor: claim.heldBy,
1137
+ participantKind: claim.participantKind,
1138
+ action: claim.reason,
1125
1139
  ...(description ? { description } : {}),
1126
- field: intent.target.field,
1140
+ field: claim.target.field,
1127
1141
  status: 'active',
1128
- expiresAt: intent.expiresAt,
1142
+ expiresAt: claim.expiresAt,
1129
1143
  target: {
1130
- model: intent.target.type,
1131
- id: intent.target.id,
1132
- path: intent.target.path,
1133
- range: intent.target.range,
1134
- field: intent.target.field,
1135
- meta: intent.target.meta,
1144
+ model: claim.target.type,
1145
+ id: claim.target.id,
1146
+ path: claim.target.path,
1147
+ range: claim.target.range,
1148
+ field: claim.target.field,
1149
+ meta: claim.target.meta,
1136
1150
  },
1137
1151
  };
1138
1152
  }
1139
- function modelClaimFromQueued(intent) {
1153
+ function modelClaimFromQueued(claim) {
1140
1154
  return {
1141
- id: intent.id,
1142
- actor: intent.heldBy,
1143
- participantKind: intent.participantKind,
1144
- action: intent.action,
1145
- ...(intent.description ? { description: intent.description } : {}),
1146
- field: intent.target.field,
1155
+ id: claim.id,
1156
+ actor: claim.heldBy,
1157
+ participantKind: claim.participantKind,
1158
+ action: claim.action,
1159
+ ...(claim.description ? { description: claim.description } : {}),
1160
+ field: claim.target.field,
1147
1161
  status: 'queued',
1148
- position: intent.position,
1149
- expiresAt: intent.expiresAt,
1162
+ position: claim.position,
1163
+ expiresAt: claim.expiresAt,
1150
1164
  target: {
1151
- model: intent.target.type,
1152
- id: intent.target.id,
1153
- path: intent.target.path,
1154
- range: intent.target.range,
1155
- field: intent.target.field,
1156
- meta: intent.target.meta,
1165
+ model: claim.target.type,
1166
+ id: claim.target.id,
1167
+ path: claim.target.path,
1168
+ range: claim.target.range,
1169
+ field: claim.target.field,
1170
+ meta: claim.target.meta,
1157
1171
  },
1158
1172
  };
1159
1173
  }
1160
- function targetMatchesModel(target, intent) {
1174
+ function targetMatchesModel(target, claim) {
1161
1175
  if (target.model &&
1162
- intent.target.type.toLowerCase() !== target.model.toLowerCase()) {
1176
+ claim.target.type.toLowerCase() !== target.model.toLowerCase()) {
1163
1177
  return false;
1164
1178
  }
1165
- if (target.id && intent.target.id !== target.id)
1179
+ if (target.id && claim.target.id !== target.id)
1166
1180
  return false;
1167
- if (target.field && intent.target.field !== target.field)
1181
+ if (target.field && claim.target.field !== target.field)
1168
1182
  return false;
1169
1183
  return true;
1170
1184
  }
1171
1185
  function listModelClaims(target) {
1172
- return intentStream.others
1173
- .filter((intent) => (target ? targetMatchesModel(target, intent) : true))
1186
+ return claimStream.others
1187
+ .filter((claim) => (target ? targetMatchesModel(target, claim) : true))
1174
1188
  .map(modelClaimFromActive);
1175
1189
  }
1176
1190
  function listModelClaimQueue(target) {
1177
1191
  if (!target?.model || !target.id)
1178
1192
  return [];
1179
- return publicIntents
1193
+ return publicClaims
1180
1194
  .queueFor({ type: target.model, id: target.id })
1181
- .filter((intent) => (target.field ? intent.target.field === target.field : true))
1195
+ .filter((claim) => (target.field ? claim.target.field === target.field : true))
1182
1196
  .map(modelClaimFromQueued);
1183
1197
  }
1184
- function claimedError(target, claims, code) {
1185
- const label = [target.model, target.id, target.field].filter(Boolean).join('/');
1186
- const holder = claims[0];
1187
- const suffix = holder
1188
- ? ` held by ${holder.actor} (${holder.action})`
1189
- : ' held by another participant';
1190
- return new AbloClaimedError(`Model row is claimed: ${label || 'target'}${suffix}.`, { code, claims });
1191
- }
1192
1198
  function waitForModelUnclaimed(target, options) {
1193
1199
  if (listModelClaims(target).length === 0)
1194
1200
  return Promise.resolve();
@@ -1216,8 +1222,8 @@ export function Ablo(options) {
1216
1222
  }
1217
1223
  };
1218
1224
  const onAbort = () => {
1219
- finish(() => reject(new AbloConnectionError('Intent wait aborted.', {
1220
- code: 'intent_wait_aborted',
1225
+ finish(() => reject(new AbloConnectionError('Claim wait aborted.', {
1226
+ code: 'claim_wait_aborted',
1221
1227
  cause: options?.signal?.reason,
1222
1228
  })));
1223
1229
  };
@@ -1225,7 +1231,7 @@ export function Ablo(options) {
1225
1231
  onAbort();
1226
1232
  return;
1227
1233
  }
1228
- unsubscribe = intentStream.onChange(check);
1234
+ unsubscribe = claimStream.onChange(check);
1229
1235
  options?.signal?.addEventListener('abort', onAbort, { once: true });
1230
1236
  if (options?.timeout != null) {
1231
1237
  timeoutId = setTimeout(() => {
@@ -1250,58 +1256,61 @@ export function Ablo(options) {
1250
1256
  }
1251
1257
  await waitForModelUnclaimed(target, { timeout: options?.claimedTimeout });
1252
1258
  }
1253
- function wrapIntentHandle(claim, waited = false) {
1259
+ function wrapClaimHandle(claim, waited = false) {
1254
1260
  const release = async () => {
1255
1261
  claim.revoke();
1256
1262
  };
1257
1263
  return {
1258
- id: claim.id,
1264
+ object: 'claim',
1265
+ claimId: claim.claimId,
1266
+ action: claim.action,
1267
+ target: claim.target,
1259
1268
  waited,
1260
1269
  release,
1261
1270
  revoke: claim.revoke,
1262
1271
  [Symbol.asyncDispose]: release,
1263
1272
  };
1264
1273
  }
1265
- const publicIntents = Object.assign(intentStream, {
1266
- async create(intentOptions) {
1274
+ const publicClaims = Object.assign(claimStream, {
1275
+ async create(claimOptions) {
1267
1276
  await ready();
1268
- const claim = intentStream.claim({
1269
- type: intentOptions.target.model,
1270
- id: intentOptions.target.id,
1271
- path: intentOptions.target.path,
1272
- range: intentOptions.target.range,
1273
- field: intentOptions.target.field,
1274
- meta: intentOptions.target.meta,
1277
+ const claim = claimStream.claim({
1278
+ type: claimOptions.target.model,
1279
+ id: claimOptions.target.id,
1280
+ path: claimOptions.target.path,
1281
+ range: claimOptions.target.range,
1282
+ field: claimOptions.target.field,
1283
+ meta: claimOptions.target.meta,
1275
1284
  }, {
1276
- reason: intentOptions.action,
1277
- ttl: intentOptions.ttl,
1278
- queue: intentOptions.queue,
1285
+ reason: claimOptions.action,
1286
+ ttl: claimOptions.ttl,
1287
+ queue: claimOptions.queue,
1279
1288
  });
1280
1289
  // With `queue`, the claim is only really *ours* once the server says
1281
- // so (`intent_acquired` if the target was free, `intent_granted` once
1290
+ // so (`claim_acquired` if the target was free, `claim_granted` once
1282
1291
  // we reach the head of the FIFO line). Block here on that grant so
1283
1292
  // callers — chiefly `ablo.<model>.claim` — get a handle that already
1284
1293
  // holds the lease, never a half-claimed one racing the queue.
1285
1294
  let waited = false;
1286
- if (intentOptions.queue) {
1295
+ if (claimOptions.queue) {
1287
1296
  const ws = store.getSyncWebSocket();
1288
1297
  if (ws) {
1289
1298
  try {
1290
- ({ waited } = await awaitIntentGrant(ws, claim.id, {
1291
- timeoutMs: intentOptions.waitTimeoutMs,
1292
- maxQueueDepth: intentOptions.maxQueueDepth,
1299
+ ({ waited } = await awaitClaimGrant(ws, claim.claimId, {
1300
+ timeoutMs: claimOptions.waitTimeoutMs,
1301
+ maxQueueDepth: claimOptions.maxQueueDepth,
1293
1302
  }));
1294
1303
  }
1295
1304
  catch (err) {
1296
1305
  // Gave up waiting (queue too deep, timed out, or lost) — abandon
1297
- // the queued intent so we don't leave a phantom entry in the
1306
+ // the queued claim so we don't leave a phantom entry in the
1298
1307
  // line that would block or mislead other claimers.
1299
1308
  claim.revoke();
1300
1309
  throw err;
1301
1310
  }
1302
1311
  }
1303
1312
  }
1304
- return wrapIntentHandle(claim, waited);
1313
+ return wrapClaimHandle(claim, waited);
1305
1314
  },
1306
1315
  list(target) {
1307
1316
  return listModelClaims(target);
@@ -1310,14 +1319,14 @@ export function Ablo(options) {
1310
1319
  return waitForModelUnclaimed(target, options);
1311
1320
  },
1312
1321
  });
1313
- // Build the typed proxy — one property per model. Done after publicIntents
1322
+ // Build the typed proxy — one property per model. Done after publicClaims
1314
1323
  // exists so model clients can expose workflow helpers such as
1315
1324
  // `ablo.files.edit(...)` without importing protocol wiring.
1316
1325
  const modelProxies = {};
1317
1326
  for (const [schemaKey, modelDef] of Object.entries(schema.models)) {
1318
1327
  const registeredModelName = modelDef.typename ?? schemaKey;
1319
1328
  modelProxies[schemaKey] = createModelProxy(schemaKey, registeredModelName, objectPool, syncClient, modelRegistry, hydration, {
1320
- createIntent: (intentOptions) => publicIntents.create(intentOptions),
1329
+ createClaim: (claimOptions) => publicClaims.create(claimOptions),
1321
1330
  createSnapshot: (modelKey, id) => createSnapshot({
1322
1331
  pool: objectPool,
1323
1332
  transport: store.getSyncWebSocket(),
@@ -1331,21 +1340,21 @@ export function Ablo(options) {
1331
1340
  getLastSyncId: () => syncClient.position.readFloor,
1332
1341
  entities: { [modelKey]: id },
1333
1342
  }),
1334
- queue: (target) => publicIntents.queueFor({ type: target.model, id: target.id }),
1335
- reorder: (target, order) => publicIntents.reorder({ type: target.model, id: target.id }, order),
1343
+ queue: (target) => publicClaims.queueFor({ type: target.model, id: target.id }),
1344
+ reorder: (target, order) => publicClaims.reorder({ type: target.model, id: target.id }, order),
1336
1345
  observe: (target) => {
1337
- // The live intent stream only tracks *open* (active) claims;
1346
+ // The live claim stream only tracks *open* (active) claims;
1338
1347
  // terminal states (committed / expired / canceled) drop out of
1339
1348
  // the list entirely — exactly the ephemeral coordination model.
1340
1349
  // So a present entry is, by definition, `status: 'active'`.
1341
- const held = publicIntents.list({
1350
+ const held = publicClaims.list({
1342
1351
  model: target.model,
1343
1352
  id: target.id,
1344
1353
  })[0];
1345
1354
  if (!held)
1346
1355
  return null;
1347
1356
  return {
1348
- object: 'intent',
1357
+ object: 'claim',
1349
1358
  id: held.id,
1350
1359
  status: 'active',
1351
1360
  target: {
@@ -1362,8 +1371,20 @@ export function Ablo(options) {
1362
1371
  expiresAt: held.expiresAt,
1363
1372
  };
1364
1373
  },
1365
- waitFor: (target, waitOptions) => publicIntents.waitFor({ model: target.model, id: target.id }, waitOptions),
1374
+ waitFor: (target, waitOptions) => publicClaims.waitFor({ model: target.model, id: target.id }, waitOptions),
1366
1375
  selfParticipantId: participantId,
1376
+ selfParticipantKind: kind,
1377
+ // Read-interest / write-intent enrolment for the typed surface.
1378
+ // `enterScope`/`pinScope` resolve the `{ [schemaKey]: id }` scope
1379
+ // through the SAME resolver the claim path uses, landing this client in
1380
+ // the entity-scoped group the holder's claim presence fans out on.
1381
+ // Return the store promise so the claim write path can AWAIT pinScope
1382
+ // BEFORE acquiring the lease (closing the subscribe-vs-broadcast race);
1383
+ // read-interest callers (`retrieve`/`claim.state`) still `void` it and
1384
+ // stay fire-and-forget. SOFT either way — the store swallows reconcile
1385
+ // errors so read interest never makes a read reject or stall.
1386
+ enterScope: (scope) => store.enterScope(scope),
1387
+ pinScope: (scope) => store.pinScope(scope),
1367
1388
  });
1368
1389
  }
1369
1390
  const commits = {
@@ -1375,7 +1396,7 @@ export function Ablo(options) {
1375
1396
  readAt: commitOptions.readAt,
1376
1397
  onStale: commitOptions.onStale,
1377
1398
  wait: commitOptions.wait,
1378
- intent: commitOptions.intent,
1399
+ claim: commitOptions.claim,
1379
1400
  }, 'commits.create');
1380
1401
  const clientTxId = createClientTxId(commitOptions.idempotencyKey);
1381
1402
  // A claim handle supplies the batch stale-guard defaults — same
@@ -1388,8 +1409,8 @@ export function Ablo(options) {
1388
1409
  onStale: commitOptions.onStale ?? (claim?.readAt !== undefined ? 'reject' : null),
1389
1410
  });
1390
1411
  const wait = commitOptions.wait ?? 'confirmed';
1391
- const intentId = normalizeIntentId(commitOptions.intent) ?? claim?.claimId;
1392
- void intentId; // The current wire clears intents by entity after commit.
1412
+ const claimId = normalizeClaimId(commitOptions.claimRef) ?? claim?.claimId;
1413
+ void claimId; // The current wire clears claims by entity after commit.
1393
1414
  // Route through the TransactionQueue's commit lane so the call
1394
1415
  // tolerates WS disconnects: the envelope stays in memory until
1395
1416
  // reconnect, mutationExecutor.commit() owns transport-level
@@ -1462,7 +1483,7 @@ export function Ablo(options) {
1462
1483
  const id = params.id ?? createModelId();
1463
1484
  await applyClaimedPolicy({ model: name, id }, params);
1464
1485
  return commits.create({
1465
- intent: params.intent,
1486
+ claimRef: params.claimRef,
1466
1487
  idempotencyKey: params.idempotencyKey,
1467
1488
  readAt: params.readAt,
1468
1489
  onStale: params.onStale,
@@ -1481,7 +1502,7 @@ export function Ablo(options) {
1481
1502
  async update(params) {
1482
1503
  await applyClaimedPolicy({ model: name, id: params.id }, params);
1483
1504
  return commits.create({
1484
- intent: params.intent,
1505
+ claimRef: params.claimRef,
1485
1506
  idempotencyKey: params.idempotencyKey,
1486
1507
  readAt: params.readAt,
1487
1508
  onStale: params.onStale,
@@ -1500,7 +1521,7 @@ export function Ablo(options) {
1500
1521
  async delete(params) {
1501
1522
  await applyClaimedPolicy({ model: name, id: params.id }, params);
1502
1523
  return commits.create({
1503
- intent: params.intent,
1524
+ claimRef: params.claimRef,
1504
1525
  idempotencyKey: params.idempotencyKey,
1505
1526
  readAt: params.readAt,
1506
1527
  onStale: params.onStale,
@@ -1659,7 +1680,7 @@ export function Ablo(options) {
1659
1680
  logger.warn('Error during sync engine disposal', { error: err.message });
1660
1681
  }
1661
1682
  presenceStream.dispose();
1662
- intentStream.dispose();
1683
+ claimStream.dispose();
1663
1684
  syncClient.dispose();
1664
1685
  },
1665
1686
  /**
@@ -1711,8 +1732,8 @@ export function Ablo(options) {
1711
1732
  /** Presence livestream — same socket as entity sync, no second
1712
1733
  * connection. Stable reference across the engine's lifetime. */
1713
1734
  presence: presenceStream,
1714
- /** Intent livestream — same socket. Stable reference. */
1715
- intents: publicIntents,
1735
+ /** Claim livestream — same socket. Stable reference. */
1736
+ claims: publicClaims,
1716
1737
  commits,
1717
1738
  model,
1718
1739
  /** Structured multiplayer participation — target-first, no
@@ -5,16 +5,17 @@
5
5
  * IndexedDB, no WebSocket. It maps the public Model / Claim / Commit
6
6
  * nouns directly to HTTP routes on sync-server.
7
7
  */
8
- import type { AbloOptions, CommitResource, IntentCreateOptions, IntentHandle, IntentWaitOptions, ModelClient, ModelClaim, ModelTarget } from './Ablo.js';
8
+ import type { AbloOptions, CommitResource, ClaimCreateOptions, ClaimWaitOptions, ModelClient, ModelClaim, ModelTarget } from './Ablo.js';
9
+ import type { ClaimHandle } from './createModelProxy.js';
9
10
  import type { Duration } from '../utils/duration.js';
10
11
  export type AbloApiClientOptions = Omit<AbloOptions, 'schema'> & {
11
12
  readonly schema?: null | undefined;
12
13
  readonly bootstrapBaseUrl?: string | undefined;
13
14
  };
14
- export interface AbloApiIntents {
15
- create(options: IntentCreateOptions): Promise<IntentHandle>;
15
+ export interface AbloApiClaims {
16
+ create(options: ClaimCreateOptions): Promise<ClaimHandle>;
16
17
  list(target?: Partial<ModelTarget>): Promise<readonly ModelClaim[]>;
17
- waitFor(target: Partial<ModelTarget>, options?: IntentWaitOptions): Promise<void>;
18
+ waitFor(target: Partial<ModelTarget>, options?: ClaimWaitOptions): Promise<void>;
18
19
  }
19
20
  export type CapabilityParticipantKind = 'agent' | 'system';
20
21
  export interface CapabilityCreateBaseOptions {
@@ -121,7 +122,7 @@ export interface AbloApi {
121
122
  dispose(): Promise<void>;
122
123
  purge(): Promise<void>;
123
124
  readonly capabilities: CapabilityResource;
124
- readonly intents: AbloApiIntents;
125
+ readonly claims: AbloApiClaims;
125
126
  readonly commits: CommitResource;
126
127
  model<T = Record<string, unknown>>(name: string): ModelClient<T>;
127
128
  /**