@abloatai/ablo 0.9.1 → 0.9.3

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 (79) hide show
  1. package/AGENTS.md +84 -0
  2. package/CHANGELOG.md +40 -0
  3. package/README.md +53 -27
  4. package/dist/BaseSyncedStore.d.ts +2 -36
  5. package/dist/BaseSyncedStore.js +11 -55
  6. package/dist/NetworkMonitor.js +4 -1
  7. package/dist/SyncClient.d.ts +22 -5
  8. package/dist/SyncClient.js +77 -0
  9. package/dist/SyncEngineContext.js +5 -1
  10. package/dist/agent/index.js +1 -1
  11. package/dist/api/index.d.ts +1 -1
  12. package/dist/auth/index.js +3 -1
  13. package/dist/cli.cjs +302645 -0
  14. package/dist/client/Ablo.d.ts +19 -52
  15. package/dist/client/Ablo.js +30 -106
  16. package/dist/client/ApiClient.d.ts +1 -113
  17. package/dist/client/ApiClient.js +39 -238
  18. package/dist/client/auth.js +32 -2
  19. package/dist/client/createInternalComponents.js +1 -1
  20. package/dist/client/createModelProxy.d.ts +9 -0
  21. package/dist/client/createModelProxy.js +34 -10
  22. package/dist/client/httpClient.d.ts +5 -6
  23. package/dist/client/httpClient.js +2 -3
  24. package/dist/client/index.d.ts +1 -1
  25. package/dist/client/persistence.d.ts +6 -1
  26. package/dist/client/persistence.js +1 -1
  27. package/dist/client/registerDataSource.d.ts +4 -4
  28. package/dist/client/registerDataSource.js +39 -31
  29. package/dist/client/writeOptionsSchema.d.ts +50 -0
  30. package/dist/client/writeOptionsSchema.js +57 -0
  31. package/dist/core/index.d.ts +18 -26
  32. package/dist/core/index.js +22 -46
  33. package/dist/errorCodes.d.ts +13 -0
  34. package/dist/errorCodes.js +19 -4
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.js +8 -1
  37. package/dist/interfaces/index.d.ts +14 -4
  38. package/dist/mutators/UndoManager.d.ts +48 -5
  39. package/dist/mutators/UndoManager.js +166 -1
  40. package/dist/react/AbloProvider.d.ts +18 -8
  41. package/dist/react/index.d.ts +1 -1
  42. package/dist/react/index.js +1 -1
  43. package/dist/react/useUndoScope.js +7 -0
  44. package/dist/schema/ddl.js +2 -1
  45. package/dist/schema/field.js +2 -1
  46. package/dist/schema/serialize.js +2 -1
  47. package/dist/server/commit.d.ts +4 -5
  48. package/dist/server/storage-mode.d.ts +7 -0
  49. package/dist/server/storage-mode.js +6 -0
  50. package/dist/source/adapters/drizzle.js +3 -2
  51. package/dist/source/adapters/kysely.d.ts +68 -0
  52. package/dist/source/adapters/kysely.js +210 -0
  53. package/dist/source/adapters/memory.js +2 -1
  54. package/dist/source/adapters/prisma.js +3 -2
  55. package/dist/source/index.js +2 -1
  56. package/dist/transactions/TransactionQueue.d.ts +6 -7
  57. package/dist/transactions/TransactionQueue.js +33 -9
  58. package/dist/types/streams.d.ts +2 -1
  59. package/dist/utils/duration.js +3 -2
  60. package/dist/wire/frames.d.ts +6 -8
  61. package/docs/api.md +1 -1
  62. package/docs/cli.md +17 -4
  63. package/docs/client-behavior.md +1 -1
  64. package/docs/data-sources.md +129 -125
  65. package/docs/examples/ai-sdk-tool.md +11 -5
  66. package/docs/examples/existing-python-backend.md +26 -4
  67. package/docs/examples/nextjs.md +3 -2
  68. package/docs/examples/scoped-agent.md +38 -11
  69. package/docs/guarantees.md +2 -2
  70. package/docs/identity.md +86 -59
  71. package/docs/index.md +2 -2
  72. package/docs/integration-guide.md +89 -61
  73. package/docs/mcp.md +1 -1
  74. package/docs/quickstart.md +84 -37
  75. package/docs/react.md +39 -28
  76. package/docs/schema-contract.md +2 -4
  77. package/llms-full.txt +360 -0
  78. package/llms.txt +30 -18
  79. package/package.json +23 -3
@@ -8,8 +8,8 @@
8
8
  import { AbloClaimedError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, translateHttpError, } from '../errors.js';
9
9
  import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, } from './auth.js';
10
10
  import { toSeconds } from '../utils/duration.js';
11
+ import { assertWriteOptions } from './writeOptionsSchema.js';
11
12
  const DEFAULT_AGENT_LEASE = '10m';
12
- const DEFAULT_INTENT_LEASE = '2m';
13
13
  export function createProtocolClient(options) {
14
14
  const env = readProcessEnv();
15
15
  const authInput = { options, env };
@@ -100,144 +100,6 @@ export function createProtocolClient(options) {
100
100
  schema: null,
101
101
  });
102
102
  }
103
- function createAgent(id, agentOptions) {
104
- return {
105
- id,
106
- async run(runOptions, handler) {
107
- if (runOptions.signal?.aborted) {
108
- return { status: 'cancelled' };
109
- }
110
- let capability = null;
111
- let task = null;
112
- try {
113
- const leaseOptions = agentOptions.leaseSeconds !== undefined
114
- ? { leaseSeconds: agentOptions.leaseSeconds }
115
- : { lease: agentOptions.lease ?? DEFAULT_AGENT_LEASE };
116
- capability = await capabilities.create({
117
- participantKind: 'agent',
118
- participantId: id,
119
- syncGroups: agentOptions.syncGroups ?? ['default'],
120
- operations: agentOptions.can,
121
- label: agentOptions.label ?? id,
122
- userMeta: agentOptions.userMeta,
123
- ...leaseOptions,
124
- });
125
- const agentClient = capability.client();
126
- task = await agentClient.tasks.create({
127
- prompt: runOptions.prompt,
128
- parentTaskId: runOptions.parentTaskId,
129
- surface: runOptions.surface ?? 'agent',
130
- metadata: runOptions.metadata,
131
- });
132
- const context = createAgentRunContext(agentClient, task);
133
- const value = await handler(context);
134
- await task.close({
135
- costInputTokens: runOptions.costInputTokens,
136
- costOutputTokens: runOptions.costOutputTokens,
137
- costComputeMs: runOptions.costComputeMs,
138
- });
139
- return { status: 'done', task, value };
140
- }
141
- catch (error) {
142
- if (task) {
143
- await task.close({
144
- costInputTokens: runOptions.costInputTokens,
145
- costOutputTokens: runOptions.costOutputTokens,
146
- costComputeMs: runOptions.costComputeMs,
147
- }).catch(() => { });
148
- }
149
- if (isAbortError(error) || runOptions.signal?.aborted) {
150
- return { status: 'cancelled', task: task ?? undefined, error };
151
- }
152
- return { status: 'failed', task: task ?? undefined, error };
153
- }
154
- finally {
155
- if (capability) {
156
- await capabilities.revoke(capability.id).catch(() => { });
157
- }
158
- }
159
- },
160
- };
161
- }
162
- function createAgentRunContext(agentClient, task) {
163
- return {
164
- task,
165
- ablo: agentClient,
166
- model(name) {
167
- return createAgentModelClient(agentClient, name);
168
- },
169
- };
170
- }
171
- function createAgentModelClient(agentClient, name) {
172
- const base = agentClient.model(name);
173
- return {
174
- retrieve(params) {
175
- // Reads are never blocked by a claim (coordination.md): a claim
176
- // serializes WRITERS, not readers. So — unlike the create/update/
177
- // delete paths below — retrieve does NOT apply the agent claimed
178
- // default; options pass through and the read path's `'return'`
179
- // default keeps a claimed row readable. A caller can still opt into
180
- // gating with an explicit `ifClaimed` (developer's choice).
181
- return base.retrieve(params);
182
- },
183
- create(params) {
184
- const id = params.id ?? createModelId();
185
- return withAgentIntent(agentClient, name, id, params, (commitIntent) => base.create({
186
- ...stripAgentRuntimeOptions(params),
187
- id,
188
- data: params.data,
189
- intent: commitIntent,
190
- }));
191
- },
192
- update(params) {
193
- return withAgentIntent(agentClient, name, params.id, params, (commitIntent) => base.update({
194
- ...stripAgentRuntimeOptions(params),
195
- id: params.id,
196
- data: params.data,
197
- intent: commitIntent,
198
- }));
199
- },
200
- delete(params) {
201
- return withAgentIntent(agentClient, name, params.id, params, (commitIntent) => base.delete({
202
- ...stripAgentRuntimeOptions(params),
203
- id: params.id,
204
- intent: commitIntent,
205
- }));
206
- },
207
- };
208
- }
209
- async function withAgentIntent(agentClient, modelName, id, mutationOptions, commit) {
210
- const intentInput = mutationOptions?.intent;
211
- const targetOverride = intentInput != null && typeof intentInput === 'object' && !isIntentHandleRef(intentInput)
212
- ? intentInput.target ?? {}
213
- : {};
214
- const target = {
215
- ...targetOverride,
216
- model: targetOverride.model ?? modelName,
217
- id: targetOverride.id ?? id,
218
- ...(intentInput != null && typeof intentInput === 'object' && !isIntentHandleRef(intentInput) && intentInput.field
219
- ? { field: intentInput.field }
220
- : {}),
221
- };
222
- await applyClaimedPolicy(target, withAgentClaimedDefault(mutationOptions), 'wait');
223
- if (intentInput == null || isIntentHandleRef(intentInput)) {
224
- return commit(intentInput);
225
- }
226
- const action = typeof intentInput === 'string' ? intentInput : intentInput.action;
227
- const intent = await agentClient.intents.create({
228
- target,
229
- action,
230
- ttl: typeof intentInput === 'object'
231
- ? intentInput.ttl ?? DEFAULT_INTENT_LEASE
232
- : DEFAULT_INTENT_LEASE,
233
- });
234
- try {
235
- return await commit(intent);
236
- }
237
- finally {
238
- await intent.release().catch(() => { });
239
- }
240
- }
241
103
  function normalizeCommitOperation(op, defaults) {
242
104
  const model = op.model ?? op.target?.model;
243
105
  if (!model) {
@@ -362,15 +224,30 @@ export function createProtocolClient(options) {
362
224
  }
363
225
  const commits = {
364
226
  async create(commitOptions) {
227
+ // Same runtime contract as every other write door — one schema.
228
+ assertWriteOptions({
229
+ idempotencyKey: commitOptions.idempotencyKey,
230
+ readAt: commitOptions.readAt,
231
+ onStale: commitOptions.onStale,
232
+ wait: commitOptions.wait,
233
+ intent: commitOptions.intent,
234
+ }, 'commits.create');
365
235
  const clientTxId = createClientTxId(commitOptions.idempotencyKey);
366
- const operations = normalizeCommitOperations(commitOptions);
236
+ // Same claim vocabulary as the WS client's `commits.create`: a handle
237
+ // supplies the batch stale-guard defaults; explicit options win.
238
+ const claim = commitOptions.claim ?? null;
239
+ const operations = normalizeCommitOperations({
240
+ ...commitOptions,
241
+ readAt: commitOptions.readAt ?? claim?.readAt ?? null,
242
+ onStale: commitOptions.onStale ?? (claim?.readAt !== undefined ? 'reject' : null),
243
+ });
367
244
  const body = await requestJson('/v1/commits', {
368
245
  method: 'POST',
369
246
  idempotencyKey: clientTxId,
370
247
  body: JSON.stringify({
371
248
  clientTxId,
372
249
  idempotencyKey: clientTxId,
373
- intent: normalizeIntentId(commitOptions.intent),
250
+ intent: normalizeIntentId(commitOptions.intent) ?? claim?.claimId,
374
251
  operations,
375
252
  }),
376
253
  });
@@ -476,51 +353,6 @@ export function createProtocolClient(options) {
476
353
  return capabilities.create(options);
477
354
  },
478
355
  };
479
- const tasks = {
480
- async create(taskOptions) {
481
- const body = await requestJson('/v1/tasks', {
482
- method: 'POST',
483
- body: JSON.stringify({
484
- prompt: taskOptions.prompt,
485
- parentTaskId: taskOptions.parentTaskId,
486
- surface: taskOptions.surface,
487
- metadata: taskOptions.metadata,
488
- }),
489
- });
490
- const id = body.id ?? body.taskId ?? body.turnId;
491
- if (!id) {
492
- throw new AbloValidationError('Task create response did not include an id.', { code: 'task_id_missing' });
493
- }
494
- return {
495
- id,
496
- turnId: id,
497
- promptHash: body.promptHash,
498
- openedAt: body.openedAt,
499
- close: (stats) => tasks.close(id, stats),
500
- };
501
- },
502
- async close(id, stats) {
503
- const body = await requestJson(`/v1/tasks/${encodeURIComponent(id)}/close`, {
504
- method: 'POST',
505
- body: JSON.stringify({
506
- costInputTokens: stats?.costInputTokens ?? 0,
507
- costOutputTokens: stats?.costOutputTokens ?? 0,
508
- costComputeMs: stats?.costComputeMs ?? 0,
509
- }),
510
- });
511
- const closedId = body.id ?? body.taskId ?? body.turnId ?? id;
512
- return {
513
- id: closedId,
514
- turnId: closedId,
515
- closed: body.closed ?? body.alreadyClosed ?? true,
516
- alreadyClosed: body.alreadyClosed,
517
- endedAt: body.endedAt,
518
- };
519
- },
520
- open(options) {
521
- return tasks.create(options);
522
- },
523
- };
524
356
  const intents = {
525
357
  async create(intentOptions) {
526
358
  const intentId = createIntentId();
@@ -619,17 +451,33 @@ export function createProtocolClient(options) {
619
451
  * envelopes — this helper is the one-op, one-record path only.
620
452
  */
621
453
  async function mutateModel(action, modelName, id, data, options) {
454
+ assertWriteOptions(options && {
455
+ idempotencyKey: options.idempotencyKey,
456
+ readAt: options.readAt,
457
+ onStale: options.onStale,
458
+ wait: options.wait,
459
+ intent: options.intent,
460
+ }, `${modelName} ${action}`);
622
461
  const clientTxId = createClientTxId(options?.idempotencyKey);
623
462
  const encModel = encodeURIComponent(modelName);
624
463
  const path = action === 'create'
625
464
  ? `/v1/models/${encModel}`
626
465
  : `/v1/models/${encModel}/${encodeURIComponent(id)}`;
627
466
  const method = action === 'create' ? 'POST' : action === 'update' ? 'PATCH' : 'DELETE';
467
+ // A carried claim handle supplies the stale-guard defaults — one claim
468
+ // vocabulary across the WS proxy, `commits.create`, and these routes.
469
+ const claimHandle = typeof options?.claim === 'object' &&
470
+ options?.claim !== null &&
471
+ options.claim.object === 'claim' &&
472
+ typeof options.claim.claimId === 'string'
473
+ ? options.claim
474
+ : undefined;
475
+ const readAt = options?.readAt ?? claimHandle?.readAt;
628
476
  const requestBody = {
629
477
  idempotencyKey: clientTxId,
630
- intent: normalizeIntentId(options?.intent),
631
- onStale: options?.onStale,
632
- readAt: options?.readAt,
478
+ intent: normalizeIntentId(options?.intent) ?? claimHandle?.claimId,
479
+ onStale: options?.onStale ?? (claimHandle?.readAt !== undefined ? 'reject' : undefined),
480
+ readAt,
633
481
  };
634
482
  if (action === 'create')
635
483
  requestBody.id = id;
@@ -687,11 +535,12 @@ export function createProtocolClient(options) {
687
535
  const releaseClaim = (params) => requestJson(claimPath(isClaimHandle(params) ? params.target.id : params.id), { method: 'DELETE' }).then(() => undefined);
688
536
  async function claimImpl(params) {
689
537
  const claimId = await acquireClaim(params);
690
- const { data } = await retrieveModel(name, { id: params.id });
538
+ const { data, stamp } = await retrieveModel(name, { id: params.id });
691
539
  const release = () => releaseClaim(params);
692
540
  return {
693
541
  object: 'claim',
694
542
  claimId,
543
+ readAt: stamp,
695
544
  target: {
696
545
  model: name,
697
546
  id: params.id,
@@ -780,35 +629,14 @@ export function createProtocolClient(options) {
780
629
  async dispose() { },
781
630
  async purge() { },
782
631
  capabilities,
783
- tasks,
784
632
  intents,
785
633
  commits,
786
634
  model,
787
- agent: createAgent,
788
635
  async getAuthToken() {
789
636
  // Mirror `authHeaders()`: a configured API key wins, else the
790
637
  // construction-time auth token. Resolve the (possibly async) key setter.
791
638
  return (await resolveApiKeyValue(configuredApiKey)) ?? configuredAuthToken ?? null;
792
639
  },
793
- async beginTurn(turnOptions) {
794
- const task = await tasks.create(turnOptions);
795
- let closed = false;
796
- const close = async (stats) => {
797
- if (closed)
798
- return;
799
- closed = true;
800
- await task.close(stats);
801
- };
802
- const dispose = () => {
803
- closed = true;
804
- };
805
- return {
806
- turnId: task.id,
807
- close,
808
- dispose,
809
- [Symbol.asyncDispose]: close,
810
- };
811
- },
812
640
  };
813
641
  }
814
642
  function normalizeIntentId(intent) {
@@ -816,33 +644,6 @@ function normalizeIntentId(intent) {
816
644
  return intent;
817
645
  return intent?.id;
818
646
  }
819
- function withAgentClaimedDefault(options) {
820
- return {
821
- ifClaimed: 'fail',
822
- ...(options ?? {}),
823
- };
824
- }
825
- function stripAgentRuntimeOptions(options) {
826
- if (!options)
827
- return undefined;
828
- const { intent: _intent, ifClaimed: _ifClaimed, claimedTimeout: _claimedTimeout, claimedPollInterval: _claimedPollInterval, maxQueueDepth: _maxQueueDepth, ...rest } = options;
829
- return rest;
830
- }
831
- function isIntentHandleRef(input) {
832
- return (typeof input === 'object' &&
833
- input !== null &&
834
- 'id' in input &&
835
- typeof input.id === 'string' &&
836
- !('action' in input));
837
- }
838
- function isAbortError(error) {
839
- return (typeof DOMException !== 'undefined' &&
840
- error instanceof DOMException &&
841
- error.name === 'AbortError') || (typeof error === 'object' &&
842
- error !== null &&
843
- 'name' in error &&
844
- error.name === 'AbortError');
845
- }
846
647
  function parseBody(bodyText) {
847
648
  if (bodyText.length === 0)
848
649
  return null;
@@ -146,8 +146,38 @@ export function resolveBootstrapBaseUrl(input) {
146
146
  // legitimately arrive as `wss://…` — normalize it here rather than
147
147
  // faceplanting at fetch time. The derive branch below already does this;
148
148
  // the override branch silently skipped it.
149
- return normalizeAbloHostedBaseUrl(input.bootstrapBaseUrl).replace(/^ws/, 'http');
149
+ return ensureApiSuffix(normalizeAbloHostedBaseUrl(input.bootstrapBaseUrl).replace(/^ws/, 'http'));
150
150
  }
151
151
  const url = normalizeAbloHostedBaseUrl(input.url);
152
- return `${url.replace(/^ws/, 'http')}/api`;
152
+ return ensureApiSuffix(url.replace(/^ws/, 'http'));
153
+ }
154
+ /**
155
+ * Guarantee the HTTP base ends in the `/api` route segment the sync-server
156
+ * mounts every endpoint under (`apps/sync-server/src/index.ts` — `app.route('/api', …)`).
157
+ *
158
+ * The derive branch always appended `/api`; the override branch did NOT,
159
+ * trusting the caller (apps/web passes `${baseUrl}/api`). But a hosted
160
+ * customer setting a custom `baseURL`/`bootstrapBaseUrl` (their own subdomain,
161
+ * staging, etc.) without the suffix sent every credential exchange to
162
+ * `…/auth/capability` instead of `…/api/auth/capability` → a 404 surfaced as
163
+ * `exchange_failed`. Since the SDK hardcodes routes relative to this base and
164
+ * there is no valid Ablo deployment that serves them off the root, normalizing
165
+ * to a single trailing `/api` here is always correct — and idempotent for
166
+ * callers who already include it.
167
+ */
168
+ function ensureApiSuffix(httpBase) {
169
+ const trimmed = httpBase.replace(/\/+$/, '');
170
+ try {
171
+ const u = new URL(trimmed);
172
+ const segments = u.pathname.split('/').filter(Boolean);
173
+ if (segments[segments.length - 1] === 'api')
174
+ return trimmed;
175
+ u.pathname = `${u.pathname.replace(/\/+$/, '')}/api`;
176
+ return u.toString().replace(/\/+$/, '');
177
+ }
178
+ catch {
179
+ // Should be unreachable post-`normalizeAbloHostedBaseUrl` (which yields an
180
+ // absolute URL), but fall back to a string check rather than throwing.
181
+ return /\/api$/.test(trimmed) ? trimmed : `${trimmed}/api`;
182
+ }
153
183
  }
@@ -42,7 +42,7 @@ export function createInternalComponents(input) {
42
42
  const database = new Database(modelRegistry, bootstrapHelper, {
43
43
  // Point-solution default: no browser-local durable store unless the
44
44
  // caller explicitly asks for it. Node/edge runtimes always use the
45
- // volatile store because IndexedDB is unavailable there.
45
+ // in-memory store because IndexedDB is unavailable there.
46
46
  inMemory: shouldUseInMemoryPersistence(options),
47
47
  });
48
48
  const syncClient = new SyncClient(objectPool, database);
@@ -212,6 +212,15 @@ export interface ClaimReorderParams<T = Record<string, unknown>> extends ClaimLo
212
212
  export interface ClaimHandle<T = Record<string, unknown>> extends AsyncDisposable {
213
213
  readonly object: 'claim';
214
214
  readonly claimId: string;
215
+ /**
216
+ * Sync watermark of the held snapshot (`data` was read at this stamp).
217
+ * Writes that carry the handle — `update({ id, data, claim })` or
218
+ * `commits.create({ claim, ... })` — use it as the `readAt` stale guard,
219
+ * so a concurrent commit between snapshot and write is rejected instead
220
+ * of clobbered. Optional for wire/duck-type compat with externally
221
+ * constructed handles.
222
+ */
223
+ readonly readAt?: number;
215
224
  readonly target: {
216
225
  readonly model: string;
217
226
  readonly id: string;
@@ -17,6 +17,7 @@
17
17
  import { autorun } from 'mobx';
18
18
  import { AbloClaimedError, AbloValidationError, toAbloError, } from '../errors.js';
19
19
  import { Model, modelAsRow } from '../Model.js';
20
+ import { assertWriteOptions } from './writeOptionsSchema.js';
20
21
  import { ModelScope } from '../types/index.js';
21
22
  const modelClientMeta = new WeakMap();
22
23
  export function getModelClientMeta(modelClient) {
@@ -76,6 +77,10 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
76
77
  };
77
78
  const mutationOptions = (params) => {
78
79
  const { id: _id, data: _data, claim: _claim, ...rest } = params;
80
+ // THE write-options schema — runtime twin of the compile-time params.
81
+ // Catches plain-JS callers (`onStale: 'rejct'`) at the call site with
82
+ // a typed error instead of a silent no-op or a server 400.
83
+ assertWriteOptions(rest, `${schemaKey} write`);
79
84
  return rest;
80
85
  };
81
86
  const releaseClaim = async (id) => {
@@ -154,6 +159,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
154
159
  return {
155
160
  object: 'claim',
156
161
  claimId: lease.id,
162
+ readAt: snapshot.stamp,
157
163
  target,
158
164
  action: options?.action ?? 'editing',
159
165
  ...(options?.description ? { description: options.description } : {}),
@@ -235,8 +241,6 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
235
241
  return this.getAll(options).length;
236
242
  },
237
243
  create: guard(async (params) => {
238
- // TODO(options-persistence): stash `params` alongside the
239
- // queued transaction so idempotencyKey survives offline flush.
240
244
  const id = params.id ?? Model.generateId();
241
245
  const opts = mutationOptions(params);
242
246
  const claim = params.claim;
@@ -260,8 +264,15 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
260
264
  maxQueueDepth: claim.maxQueueDepth,
261
265
  });
262
266
  }
267
+ // Default `organizationId` from the client's identity exactly like the
268
+ // mutator path (`buildModelForCreate`) — without this, a caller that
269
+ // omits it creates an org-unscoped row on one write door but not the
270
+ // other. An explicit value in `data` still wins via the spread.
271
+ const orgDefault = params.data.organizationId ??
272
+ syncClient.getOrganizationId();
263
273
  const model = new ModelClass({
264
274
  id,
275
+ ...(orgDefault != null ? { organizationId: orgDefault } : {}),
265
276
  ...params.data,
266
277
  createdAt: new Date(),
267
278
  updatedAt: new Date(),
@@ -299,9 +310,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
299
310
  // watermark + lease so it's stale-rejected and attributed to the claim.
300
311
  const claimed = activeClaims.get(id);
301
312
  const opts = mutationOptions(params);
302
- const handleIntent = isClaimHandle(params.claim)
303
- ? { id: params.claim.claimId }
304
- : undefined;
313
+ const handle = isClaimHandle(params.claim) ? params.claim : undefined;
305
314
  const effective = claimed
306
315
  ? {
307
316
  wait: 'confirmed',
@@ -311,8 +320,18 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
311
320
  ...opts,
312
321
  }
313
322
  : {
323
+ // A carried handle engages the same stale guard as a claim this
324
+ // proxy took itself — the watermark rides on the handle, so it
325
+ // works across clients (HTTP-minted handles included).
326
+ ...(handle?.readAt !== undefined
327
+ ? {
328
+ wait: 'confirmed',
329
+ readAt: handle.readAt,
330
+ onStale: 'reject',
331
+ }
332
+ : {}),
314
333
  ...opts,
315
- ...(handleIntent ? { intent: handleIntent } : {}),
334
+ ...(handle ? { intent: { id: handle.claimId } } : {}),
316
335
  };
317
336
  // Local user update: `applyChanges` keeps change tracking ON so
318
337
  // the edited fields land in `modifiedProperties` and actually get
@@ -341,9 +360,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
341
360
  throw new AbloValidationError(`Entity not found: ${registeredModelName}/${id}`, { code: 'entity_not_found' });
342
361
  const claimed = activeClaims.get(id);
343
362
  const opts = mutationOptions(params);
344
- const handleIntent = isClaimHandle(params.claim)
345
- ? { id: params.claim.claimId }
346
- : undefined;
363
+ const handle = isClaimHandle(params.claim) ? params.claim : undefined;
347
364
  const effective = claimed
348
365
  ? {
349
366
  wait: 'confirmed',
@@ -353,8 +370,15 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
353
370
  ...opts,
354
371
  }
355
372
  : {
373
+ ...(handle?.readAt !== undefined
374
+ ? {
375
+ wait: 'confirmed',
376
+ readAt: handle.readAt,
377
+ onStale: 'reject',
378
+ }
379
+ : {}),
356
380
  ...opts,
357
- ...(handleIntent ? { intent: handleIntent } : {}),
381
+ ...(handle ? { intent: { id: handle.claimId } } : {}),
358
382
  };
359
383
  syncClient.delete(model, effective);
360
384
  await waitForMutation(model, effective);
@@ -22,8 +22,8 @@
22
22
  * wraps it in a typed proxy facade so server code gets the SAME `client.<model>`
23
23
  * surface as the browser client — typed proxies, stateless transport.
24
24
  */
25
- import { type AbloApiClientOptions, type TaskCreateOptions } from './ApiClient.js';
26
- import type { CommitReceipt, CommitResource, HttpClaimApi, ModelRead, ModelReadOptions, Turn } from './Ablo.js';
25
+ import { type AbloApiClientOptions } from './ApiClient.js';
26
+ import type { CommitReceipt, CommitResource, HttpClaimApi, ModelRead, ModelReadOptions } from './Ablo.js';
27
27
  import type { ModelCreateParams, ModelDeleteParams, ModelLoadOptions, ModelRetrieveParams, ModelUpdateParams } from './createModelProxy.js';
28
28
  import type { Schema, SchemaRecord, InferModel, InferCreate } from '../schema/schema.js';
29
29
  export interface AbloHttpClientOptions<S extends SchemaRecord> extends Omit<AbloApiClientOptions, 'schema'> {
@@ -47,7 +47,7 @@ export interface HttpModelClient<T, C = T> {
47
47
  }
48
48
  /**
49
49
  * The honest type of the stateless HTTP client: typed model proxies (the
50
- * request/response subset) + `commits` + `beginTurn` + `dispose`. Reaching for a
50
+ * request/response subset) + `commits` + `dispose`. Reaching for a
51
51
  * stateful-only capability (`get`/`getAll`/`getCount`, `onChange`,
52
52
  * `claim.state`/`queue`/`reorder`) is a COMPILE error here, not a latent runtime
53
53
  * `undefined` — the type matches what the transport can actually do.
@@ -56,7 +56,6 @@ export type AbloHttpClient<S extends SchemaRecord> = {
56
56
  readonly [K in keyof S & string]: HttpModelClient<InferModel<Schema<S>, K>, InferCreate<Schema<S>, K>>;
57
57
  } & {
58
58
  readonly commits: CommitResource;
59
- beginTurn(options: TaskCreateOptions): Promise<Turn>;
60
59
  dispose(): Promise<void>;
61
60
  /** Resolve the bearer credential this client authenticates with (see `AbloApi.getAuthToken`). */
62
61
  getAuthToken(): Promise<string | null>;
@@ -65,7 +64,7 @@ export type AbloHttpClient<S extends SchemaRecord> = {
65
64
  };
66
65
  /**
67
66
  * Stateless, typed HTTP client. Each `client.<model>` resolves to the protocol
68
- * client's `model(name)`; `commits`, `beginTurn`, `tasks`, `dispose`, etc. pass
69
- * through. No socket is ever opened; identity is the Bearer credential.
67
+ * client's `model(name)`; `commits`, `dispose`, etc. pass through. No socket is
68
+ * ever opened; identity is the Bearer credential.
70
69
  */
71
70
  export declare function createAbloHttpClient<S extends SchemaRecord>(options: AbloHttpClientOptions<S>): AbloHttpClient<S>;
@@ -36,14 +36,13 @@ const PROTOCOL_MEMBERS = new Set([
36
36
  'dispose',
37
37
  'purge',
38
38
  'commits',
39
- 'beginTurn',
40
39
  'model',
41
40
  'getAuthToken',
42
41
  ]);
43
42
  /**
44
43
  * Stateless, typed HTTP client. Each `client.<model>` resolves to the protocol
45
- * client's `model(name)`; `commits`, `beginTurn`, `tasks`, `dispose`, etc. pass
46
- * through. No socket is ever opened; identity is the Bearer credential.
44
+ * client's `model(name)`; `commits`, `dispose`, etc. pass through. No socket is
45
+ * ever opened; identity is the Bearer credential.
47
46
  */
48
47
  export function createAbloHttpClient(options) {
49
48
  // The schema is type-level only; the protocol client is schema-agnostic.
@@ -32,5 +32,5 @@
32
32
  export { Ablo, computeFKDepthPriority, type AbloOptions, type InternalAbloOptions, type ClaimedOptions, type IfClaimedPolicy, type IntentWaitOptions, type ModelCountOptions, type ModelListOptions, type ModelListScope, type ModelLoadOptions, type ModelOperations, type ModelReadOptions, } from './Ablo.js';
33
33
  export { ABLO_DEFAULT_BASE_URL, ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_URL, normalizeAbloHostedBaseUrl, } from './auth.js';
34
34
  export type { AbloPersistence } from './persistence.js';
35
- export type { AbloApi, AbloApiClientOptions, AbloApiIntents, Agent, AgentIntentInput, AgentIntentOptions, AgentOptions, AgentModelClient, AgentModelReadOptions, AgentModelMutationOptions, AgentRunContext, AgentRunDone, AgentRunFailed, AgentRunCancelled, AgentRunOptions, AgentRunResult, AgentRunStatus, Capability, CapabilityCreateOptions, CapabilityParticipantKind, CapabilityRecord, CapabilityResource, CapabilityRevocation, CapabilityScope, Task, TaskCloseOptions, TaskCloseResult, TaskCreateOptions, TaskResource, } from './ApiClient.js';
35
+ export type { AbloApi, AbloApiClientOptions, AbloApiIntents, Capability, CapabilityCreateOptions, CapabilityParticipantKind, CapabilityRecord, CapabilityResource, CapabilityRevocation, CapabilityScope, } from './ApiClient.js';
36
36
  export type { EngineParticipant, JoinedParticipant, ParticipantJoinOptions, ParticipantManager, ParticipantScope, ParticipantStatus, ScopedIntents, ScopedPresence, } from '../sync/participants.js';
@@ -1,4 +1,9 @@
1
- export type AbloPersistence = 'volatile' | 'indexeddb';
1
+ /**
2
+ * Local persistence modes. `'memory'` (the default everywhere outside the
3
+ * browser) keeps the local graph in process memory; `'indexeddb'` adds
4
+ * offline queueing and a reload-surviving cache in the browser.
5
+ */
6
+ export type AbloPersistence = 'memory' | 'indexeddb';
2
7
  export interface PersistenceOptions {
3
8
  readonly persistence?: AbloPersistence | undefined;
4
9
  readonly inMemory?: boolean | undefined;
@@ -2,7 +2,7 @@ export function shouldUseInMemoryPersistence(options) {
2
2
  if (typeof window === 'undefined')
3
3
  return true;
4
4
  if (options.persistence)
5
- return options.persistence === 'volatile';
5
+ return options.persistence === 'memory';
6
6
  if (typeof options.inMemory === 'boolean')
7
7
  return options.inMemory;
8
8
  if (options.offline === true)
@@ -11,9 +11,9 @@ export interface RegisterDataSourceInput {
11
11
  readonly fetchImpl?: typeof fetch;
12
12
  }
13
13
  /**
14
- * POST the connection string to the self-serve direct connector route. Resolves
15
- * on success (the org is now a dedicated tenant pointed at this DB); throws an
16
- * `AbloError` with `datasource_registration_failed` otherwise so `ready()`
17
- * surfaces it instead of silently bootstrapping against the wrong store.
14
+ * POST the connection string to the self-serve datasource route. Resolves on
15
+ * success (the org's data plane now points at this DB); throws an `AbloError`
16
+ * with `datasource_registration_failed` otherwise so `ready()` surfaces it
17
+ * instead of silently bootstrapping against the wrong store.
18
18
  */
19
19
  export declare function registerDataSource(input: RegisterDataSourceInput): Promise<void>;