@abloatai/ablo 0.8.0 → 0.9.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 (165) hide show
  1. package/CHANGELOG.md +46 -1
  2. package/README.md +33 -28
  3. package/dist/BaseSyncedStore.d.ts +83 -0
  4. package/dist/BaseSyncedStore.js +194 -2
  5. package/dist/Model.d.ts +42 -0
  6. package/dist/Model.js +103 -44
  7. package/dist/agent/session.js +3 -3
  8. package/dist/ai-sdk/coordination-context.js +4 -0
  9. package/dist/ai-sdk/index.d.ts +56 -47
  10. package/dist/ai-sdk/index.js +56 -47
  11. package/dist/ai-sdk/intent-broadcast.d.ts +5 -0
  12. package/dist/ai-sdk/intent-broadcast.js +11 -4
  13. package/dist/ai-sdk/wrap.d.ts +14 -11
  14. package/dist/ai-sdk/wrap.js +11 -13
  15. package/dist/auth/credentialSource.d.ts +34 -0
  16. package/dist/auth/credentialSource.js +63 -0
  17. package/dist/auth/index.d.ts +2 -22
  18. package/dist/auth/index.js +4 -42
  19. package/dist/auth/schemas.d.ts +35 -0
  20. package/dist/auth/schemas.js +53 -0
  21. package/dist/client/Ablo.d.ts +160 -42
  22. package/dist/client/Ablo.js +145 -75
  23. package/dist/client/ApiClient.d.ts +20 -4
  24. package/dist/client/ApiClient.js +166 -28
  25. package/dist/client/auth.d.ts +14 -5
  26. package/dist/client/auth.js +60 -7
  27. package/dist/client/createInternalComponents.d.ts +2 -0
  28. package/dist/client/createInternalComponents.js +8 -1
  29. package/dist/client/createModelProxy.d.ts +130 -66
  30. package/dist/client/createModelProxy.js +152 -49
  31. package/dist/client/httpClient.d.ts +71 -0
  32. package/dist/client/httpClient.js +69 -0
  33. package/dist/client/identity.d.ts +2 -6
  34. package/dist/client/identity.js +49 -11
  35. package/dist/client/index.d.ts +1 -0
  36. package/dist/client/index.js +1 -0
  37. package/dist/client/registerDataSource.d.ts +3 -3
  38. package/dist/client/registerDataSource.js +11 -9
  39. package/dist/client/validateAbloOptions.js +1 -1
  40. package/dist/core/DatabaseManager.js +30 -2
  41. package/dist/core/openIDBWithTimeout.d.ts +36 -0
  42. package/dist/core/openIDBWithTimeout.js +88 -1
  43. package/dist/errorCodes.d.ts +70 -1
  44. package/dist/errorCodes.js +108 -9
  45. package/dist/errors.d.ts +2 -2
  46. package/dist/errors.js +72 -22
  47. package/dist/index.d.ts +17 -8
  48. package/dist/index.js +15 -6
  49. package/dist/keys/index.d.ts +16 -1
  50. package/dist/keys/index.js +26 -6
  51. package/dist/mutators/UndoManager.d.ts +158 -50
  52. package/dist/mutators/UndoManager.js +345 -22
  53. package/dist/mutators/inverseOp.d.ts +129 -0
  54. package/dist/mutators/inverseOp.js +74 -0
  55. package/dist/mutators/readerActions.d.ts +1 -1
  56. package/dist/mutators/undoApply.d.ts +42 -0
  57. package/dist/mutators/undoApply.js +143 -0
  58. package/dist/query/client.d.ts +10 -9
  59. package/dist/query/client.js +3 -6
  60. package/dist/react/AbloProvider.d.ts +23 -126
  61. package/dist/react/AbloProvider.js +62 -199
  62. package/dist/react/context.d.ts +31 -0
  63. package/dist/react/useAblo.d.ts +2 -2
  64. package/dist/react/useCurrentUserId.d.ts +1 -1
  65. package/dist/react/useCurrentUserId.js +1 -1
  66. package/dist/react/useMutators.js +19 -12
  67. package/dist/schema/ddl.d.ts +34 -3
  68. package/dist/schema/ddl.js +162 -4
  69. package/dist/schema/index.d.ts +5 -1
  70. package/dist/schema/index.js +13 -1
  71. package/dist/schema/model.d.ts +11 -0
  72. package/dist/schema/model.js +2 -0
  73. package/dist/schema/openapi.d.ts +28 -0
  74. package/dist/schema/openapi.js +118 -0
  75. package/dist/schema/plane.d.ts +23 -0
  76. package/dist/schema/plane.js +19 -0
  77. package/dist/schema/relation.d.ts +20 -0
  78. package/dist/schema/serialize.d.ts +4 -0
  79. package/dist/schema/serialize.js +4 -0
  80. package/dist/schema/sync-delta-row.d.ts +157 -0
  81. package/dist/schema/sync-delta-row.js +102 -0
  82. package/dist/schema/sync-delta-wire.d.ts +180 -0
  83. package/dist/schema/sync-delta-wire.js +102 -0
  84. package/dist/server/adapter.d.ts +156 -0
  85. package/dist/server/adapter.js +19 -0
  86. package/dist/server/commit.d.ts +82 -0
  87. package/dist/server/commit.js +1 -0
  88. package/dist/server/index.d.ts +14 -0
  89. package/dist/server/index.js +1 -0
  90. package/dist/server/next.d.ts +51 -0
  91. package/dist/server/next.js +47 -0
  92. package/dist/server/read-config.d.ts +60 -0
  93. package/dist/server/read-config.js +8 -0
  94. package/dist/server/storage-mode.d.ts +17 -0
  95. package/dist/server/storage-mode.js +12 -0
  96. package/dist/source/adapter.d.ts +65 -0
  97. package/dist/source/adapter.js +20 -0
  98. package/dist/source/adapters/drizzle.d.ts +43 -0
  99. package/dist/source/adapters/drizzle.js +185 -0
  100. package/dist/source/adapters/memory.d.ts +12 -0
  101. package/dist/source/adapters/memory.js +114 -0
  102. package/dist/source/adapters/prisma.d.ts +57 -0
  103. package/dist/source/adapters/prisma.js +176 -0
  104. package/dist/source/conformance.d.ts +32 -0
  105. package/dist/source/conformance.js +134 -0
  106. package/dist/source/contract.d.ts +144 -0
  107. package/dist/source/contract.js +99 -0
  108. package/dist/source/index.d.ts +62 -10
  109. package/dist/source/index.js +99 -0
  110. package/dist/source/migrations.d.ts +14 -0
  111. package/dist/source/migrations.js +39 -0
  112. package/dist/source/next.d.ts +33 -0
  113. package/dist/source/next.js +26 -0
  114. package/dist/sync/BootstrapHelper.d.ts +10 -0
  115. package/dist/sync/BootstrapHelper.js +10 -15
  116. package/dist/sync/ConnectionManager.d.ts +55 -1
  117. package/dist/sync/ConnectionManager.js +155 -16
  118. package/dist/sync/HydrationCoordinator.d.ts +93 -17
  119. package/dist/sync/HydrationCoordinator.js +238 -39
  120. package/dist/sync/NetworkProbe.d.ts +58 -24
  121. package/dist/sync/NetworkProbe.js +118 -42
  122. package/dist/sync/SyncWebSocket.d.ts +45 -70
  123. package/dist/sync/SyncWebSocket.js +70 -36
  124. package/dist/sync/createIntentStream.js +10 -1
  125. package/dist/types/streams.d.ts +9 -0
  126. package/dist/utils/mobx-setup.js +1 -0
  127. package/dist/webhooks/events.d.ts +38 -0
  128. package/dist/webhooks/events.js +40 -0
  129. package/dist/webhooks/index.d.ts +10 -0
  130. package/dist/webhooks/index.js +10 -0
  131. package/dist/wire/errorEnvelope.d.ts +34 -0
  132. package/dist/wire/errorEnvelope.js +86 -0
  133. package/dist/wire/frames.d.ts +119 -0
  134. package/dist/wire/frames.js +1 -0
  135. package/dist/wire/index.d.ts +24 -0
  136. package/dist/wire/index.js +21 -0
  137. package/dist/wire/listEnvelope.d.ts +45 -0
  138. package/dist/wire/listEnvelope.js +17 -0
  139. package/docs/api.md +47 -44
  140. package/docs/cli.md +44 -44
  141. package/docs/client-behavior.md +30 -30
  142. package/docs/coordination.md +33 -36
  143. package/docs/data-sources.md +35 -15
  144. package/docs/examples/agent-human.md +45 -43
  145. package/docs/examples/ai-sdk-tool.md +20 -16
  146. package/docs/examples/existing-python-backend.md +16 -12
  147. package/docs/examples/nextjs.md +14 -12
  148. package/docs/examples/scoped-agent.md +1 -1
  149. package/docs/examples/server-agent.md +24 -21
  150. package/docs/guarantees.md +15 -13
  151. package/docs/index.md +2 -2
  152. package/docs/integration-guide.md +30 -30
  153. package/docs/interaction-model.md +19 -23
  154. package/docs/mcp/claude-code.md +3 -3
  155. package/docs/mcp/cursor.md +1 -1
  156. package/docs/mcp/windsurf.md +2 -2
  157. package/docs/mcp.md +6 -6
  158. package/docs/quickstart.md +41 -31
  159. package/docs/react.md +13 -9
  160. package/docs/schema-contract.md +12 -10
  161. package/docs/the-loop.md +21 -0
  162. package/examples/data-source/README.md +4 -5
  163. package/examples/data-source/customer-server.ts +27 -25
  164. package/llms.txt +28 -5
  165. package/package.json +43 -3
@@ -43,13 +43,22 @@ export declare function readProcessEnv(): Record<string, string | undefined>;
43
43
  export declare function resolveApiKey(input: AuthResolveInput): string | ApiKeySetter | null;
44
44
  export declare function resolveAuthToken(input: AuthResolveInput): string | null;
45
45
  /**
46
- * Resolve the customer's own-Postgres connection string for write-back
47
- * (dedicated/BYO tenant). Falls back to `DATABASE_URL` — the Prisma-style
48
- * convention so a server-side app that already exports it needs no extra
49
- * config. Returns null for Ablo-managed storage (the hosted default).
46
+ * Resolve the direct-URL connector's Postgres connection string.
47
+ *
48
+ * The default Data Source path should not call this: the customer keeps
49
+ * `DATABASE_URL` in their app and exposes `dataSource(...)`. This helper exists
50
+ * only for the opt-in direct connector where Ablo registers a dedicated tenant
51
+ * database. Returns null for Ablo-managed storage.
50
52
  */
51
53
  export declare function resolveDatabaseUrl(input: AuthResolveInput): string | null;
52
- export declare const ABLO_DEFAULT_BASE_URL = "wss://mesh.ablo.finance";
54
+ export declare const ABLO_HOSTED_API_DOMAIN = "api.abloatai.com";
55
+ export declare const ABLO_HOSTED_HTTP_BASE_URL = "https://api.abloatai.com";
56
+ export declare const ABLO_DEFAULT_BASE_URL = "wss://api.abloatai.com";
57
+ /**
58
+ * Normalize old hosted aliases to the public API domain. Self-hosted/custom
59
+ * URLs pass through unchanged; only first-party legacy hosts are rewritten.
60
+ */
61
+ export declare function normalizeAbloHostedBaseUrl(rawUrl: string): string;
53
62
  export declare function resolveBaseURL(input: AuthResolveInput): string;
54
63
  /**
55
64
  * Browser guard — apiKey is server-side-only by default. Same check
@@ -28,17 +28,58 @@ export function resolveAuthToken(input) {
28
28
  return input.options.authToken ?? null;
29
29
  }
30
30
  /**
31
- * Resolve the customer's own-Postgres connection string for write-back
32
- * (dedicated/BYO tenant). Falls back to `DATABASE_URL` — the Prisma-style
33
- * convention so a server-side app that already exports it needs no extra
34
- * config. Returns null for Ablo-managed storage (the hosted default).
31
+ * Resolve the direct-URL connector's Postgres connection string.
32
+ *
33
+ * The default Data Source path should not call this: the customer keeps
34
+ * `DATABASE_URL` in their app and exposes `dataSource(...)`. This helper exists
35
+ * only for the opt-in direct connector where Ablo registers a dedicated tenant
36
+ * database. Returns null for Ablo-managed storage.
35
37
  */
36
38
  export function resolveDatabaseUrl(input) {
37
39
  return input.options.databaseUrl ?? input.env.DATABASE_URL ?? null;
38
40
  }
39
- export const ABLO_DEFAULT_BASE_URL = 'wss://mesh.ablo.finance';
41
+ export const ABLO_HOSTED_API_DOMAIN = 'api.abloatai.com';
42
+ export const ABLO_HOSTED_HTTP_BASE_URL = `https://${ABLO_HOSTED_API_DOMAIN}`;
43
+ export const ABLO_DEFAULT_BASE_URL = `wss://${ABLO_HOSTED_API_DOMAIN}`;
44
+ const LEGACY_HOSTED_API_HOSTS = new Set([
45
+ 'mesh.ablo.finance',
46
+ 'mesh-staging.ablo.finance',
47
+ 'api.ablo.finance',
48
+ 'sync-staging.ablo.finance',
49
+ ]);
50
+ /**
51
+ * Normalize old hosted aliases to the public API domain. Self-hosted/custom
52
+ * URLs pass through unchanged; only first-party legacy hosts are rewritten.
53
+ */
54
+ export function normalizeAbloHostedBaseUrl(rawUrl) {
55
+ const trimmed = rawUrl.trim();
56
+ if (!trimmed)
57
+ return trimmed;
58
+ // A scheme-less value (e.g. `api-staging.abloatai.com`) is a RELATIVE URL:
59
+ // `new URL()` throws on it, and downstream `fetch` then resolves it against
60
+ // the current page — producing `https://<app-host>/<route>/api-staging…/api/
61
+ // auth/identity`, a 404 from the app's own origin. Prepend a scheme so the
62
+ // base is absolute. `https` mirrors `ABLO_HOSTED_HTTP_BASE_URL`; the socket
63
+ // layer derives `wss` from it. An existing scheme (ws/wss/http/https) is
64
+ // preserved untouched.
65
+ const schemed = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
66
+ try {
67
+ const url = new URL(schemed);
68
+ if (!LEGACY_HOSTED_API_HOSTS.has(url.hostname))
69
+ return schemed.replace(/\/+$/, '');
70
+ url.hostname = ABLO_HOSTED_API_DOMAIN;
71
+ if (url.protocol === 'http:')
72
+ url.protocol = 'https:';
73
+ if (url.protocol === 'ws:')
74
+ url.protocol = 'wss:';
75
+ return url.toString().replace(/\/+$/, '');
76
+ }
77
+ catch {
78
+ return schemed;
79
+ }
80
+ }
40
81
  export function resolveBaseURL(input) {
41
- return input.options.baseURL ?? ABLO_DEFAULT_BASE_URL;
82
+ return normalizeAbloHostedBaseUrl(input.options.baseURL ?? ABLO_DEFAULT_BASE_URL);
42
83
  }
43
84
  /**
44
85
  * Browser guard — apiKey is server-side-only by default. Same check
@@ -96,5 +137,17 @@ export async function resolveApiKeyValue(apiKey) {
96
137
  * preserves the protocol family (ws → http, wss → https).
97
138
  */
98
139
  export function resolveBootstrapBaseUrl(input) {
99
- return input.bootstrapBaseUrl ?? `${input.url.replace(/^ws/, 'http')}/api`;
140
+ if (input.bootstrapBaseUrl) {
141
+ // Coerce ws/wss → http/https on the override path too. This base URL is
142
+ // used for HTTP fetches (identity resolve, apiKey exchange, bootstrap) and
143
+ // the browser `fetch` rejects ws/wss schemes outright ("URL scheme \"wss\"
144
+ // is not supported"). apps/web derives this override as `${baseUrl}/api`
145
+ // where `baseUrl` may carry a WebSocket scheme, so the override can
146
+ // legitimately arrive as `wss://…` — normalize it here rather than
147
+ // faceplanting at fetch time. The derive branch below already does this;
148
+ // the override branch silently skipped it.
149
+ return normalizeAbloHostedBaseUrl(input.bootstrapBaseUrl).replace(/^ws/, 'http');
150
+ }
151
+ const url = normalizeAbloHostedBaseUrl(input.url);
152
+ return `${url.replace(/^ws/, 'http')}/api`;
100
153
  }
@@ -16,6 +16,7 @@ import { ObjectPool } from '../ObjectPool.js';
16
16
  import { SyncClient } from '../SyncClient.js';
17
17
  import { HydrationCoordinator } from '../sync/HydrationCoordinator.js';
18
18
  import { BootstrapHelper } from '../sync/BootstrapHelper.js';
19
+ import type { AuthCredentialSource } from '../auth/credentialSource.js';
19
20
  import type { Schema, SchemaRecord } from '../schema/schema.js';
20
21
  import { type AbloPersistence } from './persistence.js';
21
22
  export interface InternalComponentsInput<S extends SchemaRecord> {
@@ -31,6 +32,7 @@ export interface InternalComponentsInput<S extends SchemaRecord> {
31
32
  readonly offline?: boolean;
32
33
  readonly inMemory?: boolean;
33
34
  };
35
+ readonly auth?: AuthCredentialSource;
34
36
  }
35
37
  export interface InternalComponents {
36
38
  readonly modelRegistry: ModelRegistry;
@@ -19,7 +19,7 @@ import { BootstrapHelper } from '../sync/BootstrapHelper.js';
19
19
  import { resolveBootstrapBaseUrl } from './auth.js';
20
20
  import { shouldUseInMemoryPersistence } from './persistence.js';
21
21
  export function createInternalComponents(input) {
22
- const { schema, url, options } = input;
22
+ const { schema, url, options, auth } = input;
23
23
  // The registry is created here but model registration happens in
24
24
  // the caller (Ablo.ts owns `registerModelsFromSchema` since the
25
25
  // schema-to-class translation depends on private helpers there).
@@ -37,6 +37,7 @@ export function createInternalComponents(input) {
37
37
  baseUrl: bootstrapBaseUrl,
38
38
  syncGroups: options.syncGroups,
39
39
  instantModels: deriveInstantModels(schema),
40
+ getAuthToken: auth?.getAuthToken,
40
41
  });
41
42
  const database = new Database(modelRegistry, bootstrapHelper, {
42
43
  // Point-solution default: no browser-local durable store unless the
@@ -55,7 +56,13 @@ export function createInternalComponents(input) {
55
56
  registry: modelRegistry,
56
57
  schema,
57
58
  baseUrl: bootstrapBaseUrl,
59
+ getAuthToken: auth?.getAuthToken,
58
60
  });
61
+ // Drop the lazy-lane hydration ledger on reconnect. While connected, the
62
+ // WebSocket delta stream keeps hydrated rows fresh so repeat reads serve
63
+ // pure-local with no network; after a drop, deltas may have been missed, so
64
+ // the next read of each query must re-confirm with the server once.
65
+ syncClient.on('sync:reconnecting', () => hydration.invalidate());
59
66
  return {
60
67
  modelRegistry,
61
68
  objectPool,
@@ -10,7 +10,7 @@
10
10
  * exposes the async server reads `retrieve` / `list`, the synchronous
11
11
  * local-graph snapshots `get` / `getAll` / `getCount`, the writes
12
12
  * `create` / `update` / `delete`, the coordination namespace `claim`
13
- * (`claim(id, work)` plus `claim.state` / `claim.queue` / `claim.release` /
13
+ * (`claim({ id })` plus `claim.state` / `claim.queue` / `claim.release` /
14
14
  * `claim.reorder`), and `onChange`. The factory returns a plain object; the
15
15
  * client assembles the `ablo.<model>` lookup table from these.
16
16
  */
@@ -21,7 +21,7 @@ import type { SyncClient } from '../SyncClient.js';
21
21
  import type { HydrationCoordinator } from '../sync/HydrationCoordinator.js';
22
22
  import type { LoadWhere } from '../query/types.js';
23
23
  import { ModelScope } from '../types/index.js';
24
- import type { Duration, Intent, IntentWaitOptions, Snapshot } from '../types/streams.js';
24
+ import type { Duration, Intent, IntentWaitOptions, Snapshot, TargetRange } from '../types/streams.js';
25
25
  export interface ModelClientMeta {
26
26
  readonly key: string;
27
27
  readonly typename: string;
@@ -70,7 +70,7 @@ export interface ModelLoadOptions<T> {
70
70
  */
71
71
  expand?: readonly string[];
72
72
  }
73
- /** Options for the single-row async server read `retrieve(id)`. A subset of
73
+ /** Options for the single-row async server read `retrieve({ id })`. A subset of
74
74
  * {@link ModelLoadOptions} — `where`/`limit`/`orderBy` are fixed by the id. */
75
75
  export type ModelRetrieveOptions = Pick<ModelLoadOptions<unknown>, 'type' | 'expand'>;
76
76
  export interface IntentLeaseHandle {
@@ -84,6 +84,9 @@ export interface ModelCollaboration<T> {
84
84
  model: string;
85
85
  id: string;
86
86
  field?: string;
87
+ path?: string;
88
+ range?: TargetRange;
89
+ meta?: Record<string, unknown>;
87
90
  };
88
91
  action: string;
89
92
  ttl?: Duration;
@@ -139,12 +142,19 @@ export interface ModelCollaboration<T> {
139
142
  */
140
143
  readonly selfParticipantId: string;
141
144
  }
142
- /** Options for `claim(id, …)`. */
143
- export interface ClaimOptions {
145
+ export interface ClaimTargetOptions<T = Record<string, unknown>> {
144
146
  /** Phase shown to observers while held. Defaults to `'editing'`. */
145
147
  action?: string;
148
+ /** Peer-visible explanation of the work being performed. */
149
+ description?: string;
146
150
  /** Field-level target, for fine-grained claimed-state badges. */
147
151
  field?: string;
152
+ /** Optional path for document/file-like targets. */
153
+ path?: string;
154
+ /** Optional range for document/file-like targets. */
155
+ range?: TargetRange;
156
+ /** App-defined structured metadata. */
157
+ meta?: Record<string, unknown>;
148
158
  /** Crash-cleanup TTL — the claim auto-releases if the holder dies. */
149
159
  ttl?: Duration;
150
160
  /**
@@ -164,73 +174,120 @@ export interface ClaimOptions {
164
174
  */
165
175
  maxQueueDepth?: number;
166
176
  }
177
+ /** Options for `claim({ id, ... })`. */
178
+ export interface ClaimParams<T = Record<string, unknown>> extends ClaimTargetOptions<T> {
179
+ readonly id: string;
180
+ }
181
+ export interface ClaimLookupParams<T = Record<string, unknown>> {
182
+ readonly id: string;
183
+ readonly field?: string;
184
+ }
185
+ export interface ClaimReorderParams<T = Record<string, unknown>> extends ClaimLookupParams<T> {
186
+ readonly order: readonly Intent[];
187
+ }
167
188
  /**
168
- * A claimed row: the entity's data plus an async-dispose hook, so
189
+ * A claim handle: the held entity data plus an explicit release hook, so
169
190
  *
170
191
  * ```ts
171
- * await ablo.weatherReports.claim('report_stockholm', async (report) => {
172
- * await ablo.weatherReports.update(report.id, { status: 'ready' });
192
+ * const claim = await ablo.weatherReports.claim({
193
+ * id: 'report_stockholm',
194
+ * action: 'forecasting',
195
+ * description: 'Fetching current weather before writing the forecast.',
173
196
  * });
197
+ * try {
198
+ * await ablo.weatherReports.update({
199
+ * id: claim.target.id,
200
+ * data: { status: 'ready' },
201
+ * claim,
202
+ * });
203
+ * } finally {
204
+ * await claim.release();
205
+ * }
174
206
  * ```
175
207
  *
176
- * releases the claim when the callback returns or throws. Read it like any row
177
- * (`report.location`); write it through the flat `ablo.<model>.update(report.id, )`
178
- * verb there is no method chaining on the claim.
208
+ * `data` is a snapshot taken after the lease is held. Write through the flat
209
+ * `ablo.<model>.update({ id, data, claim })` verb — the handle carries the
210
+ * lease id and snapshot watermark for attribution + stale protection.
179
211
  */
180
- export type ClaimedRow<T> = T & AsyncDisposable;
212
+ export interface ClaimHandle<T = Record<string, unknown>> extends AsyncDisposable {
213
+ readonly object: 'claim';
214
+ readonly claimId: string;
215
+ readonly target: {
216
+ readonly model: string;
217
+ readonly id: string;
218
+ readonly field?: string;
219
+ readonly path?: string;
220
+ readonly range?: TargetRange;
221
+ readonly meta?: Record<string, unknown>;
222
+ };
223
+ readonly action: string;
224
+ readonly description?: string;
225
+ readonly data: T;
226
+ release(): Promise<void>;
227
+ revoke(): void;
228
+ }
229
+ export type ClaimOptions<T = Record<string, unknown>> = ClaimTargetOptions<T>;
181
230
  /**
182
231
  * The coordination surface for a model, exposed as a callable namespace.
183
232
  *
184
- * Calling it takes a claim either the callback form (`claim(id, work)`, which
185
- * releases when `work` settles) or the bare lease form (`claim(id)`). Its
186
- * members read and steer the same coordination plane: `state` (current holder),
187
- * `queue` (the wait line), `release` (drop a held claim), `reorder` (re-rank the
188
- * line). All four stay on the ephemeral coordination plane — never persisted,
189
- * never a `SyncDelta`.
233
+ * Most callers do not need this namespace directly. Put `claim: { ... }` on a
234
+ * write and the SDK acquires/releases around that one mutation:
235
+ *
236
+ * ```ts
237
+ * await ablo.tasks.update({
238
+ * id,
239
+ * data: { title },
240
+ * claim: {
241
+ * field: 'title',
242
+ * action: 'renaming',
243
+ * description: 'Renaming the task to match the project brief.',
244
+ * },
245
+ * });
246
+ * ```
247
+ *
248
+ * Use `claim({ id, ... })` when a tool spans multiple writes and needs one
249
+ * handle. `state`, `queue`, and `reorder` are coordination reads/scheduler
250
+ * controls for UI and operators.
190
251
  */
191
252
  export interface ClaimApi<T> {
192
- /** Take a claim and get the held row back (bare lease form). */
193
- (id: string, options?: ClaimOptions): Promise<ClaimedRow<T>>;
194
- /**
195
- * Take a claim, run `work` with the held row, release when it settles. The
196
- * preferred form for ordinary held work.
197
- */
198
- <R>(id: string, work: (row: ClaimedRow<T>) => Promise<R> | R, options?: ClaimOptions): Promise<R>;
253
+ /** Take a claim and get an explicit held-work handle back. */
254
+ (params: ClaimParams<T>): Promise<ClaimHandle<T>>;
199
255
  /**
200
- * Who's coordinating on a row the current holder (who, phase, until when),
201
- * or `null` when free. Synchronous and reactive; for observers/UI. Never
202
- * blocks.
256
+ * Current holder for a row, or `null` when free. Use this for UI badges and
257
+ * preflight checks, not for the normal write path.
203
258
  */
204
- state(id: string): Intent | null;
259
+ state(params: ClaimLookupParams<T>): Intent | null;
205
260
  /**
206
- * The wait queue on a row — who's lined up behind the holder and what each
207
- * intends. Reactive snapshot (synced from the server, like `activity`);
208
- * returns a Stripe-style list envelope, FIFO order, empty when no one waits.
209
- *
210
- * ```ts
211
- * const { data } = ablo.decks.claim.queue('deck_1');
212
- * // → [{ heldBy: 'agent:summarizer', action: 'editing', position: 0 }, …]
213
- * ```
261
+ * FIFO wait line behind the current holder. Advanced: useful for operator
262
+ * UIs and schedulers.
214
263
  */
215
- queue(id: string): {
264
+ queue(params: ClaimLookupParams<T>): {
216
265
  readonly object: 'list';
217
266
  readonly data: readonly Intent[];
218
267
  };
219
268
  /**
220
- * Re-rank the wait queue on a record — move waiters to the front in the given
221
- * order. Pass the `Intent[]` from `claim.queue(id).data`, reordered. A
222
- * privileged operation: the server gates it (the caller needs the
223
- * `intent.reorder` capability), so it's fire-and-forget — the new order
224
- * arrives reactively through `claim.queue(id)`.
225
- *
226
- * ```ts
227
- * const { data } = ablo.decks.claim.queue('deck_1');
228
- * ablo.decks.claim.reorder('deck_1', [data[2], data[0], data[1]]); // promote #2
229
- * ```
269
+ * Re-rank the wait line. Advanced and permission-gated.
230
270
  */
231
- reorder(id: string, order: readonly Intent[]): void;
232
- /** Release a claim you hold early. Usually implicit (scope exit). */
233
- release(id: string): Promise<void>;
271
+ reorder(params: ClaimReorderParams<T>): void;
272
+ /** Release a manual claim handle early. Single-write claims auto-release. */
273
+ release(params: ClaimLookupParams<T> | ClaimHandle<T>): Promise<void>;
274
+ }
275
+ export interface ModelRetrieveParams extends ModelRetrieveOptions {
276
+ readonly id: string;
277
+ }
278
+ export interface ModelCreateParams<T, CreateInput> extends MutationOptions {
279
+ readonly data: CreateInput;
280
+ readonly id?: string | null;
281
+ readonly claim?: ClaimHandle<T> | ClaimTargetOptions<T> | null;
282
+ }
283
+ export interface ModelUpdateParams<T> extends MutationOptions {
284
+ readonly id: string;
285
+ readonly data: Partial<T>;
286
+ readonly claim?: ClaimHandle<T> | ClaimTargetOptions<T> | null;
287
+ }
288
+ export interface ModelDeleteParams<T> extends MutationOptions {
289
+ readonly id: string;
290
+ readonly claim?: ClaimHandle<T> | ClaimTargetOptions<T> | null;
234
291
  }
235
292
  export interface ModelOperations<T, CreateInput> {
236
293
  /**
@@ -244,9 +301,9 @@ export interface ModelOperations<T, CreateInput> {
244
301
  * synchronous read of an already-warm graph (a React selector) use
245
302
  * `get(id)`.
246
303
  *
247
- * Mirrors `stripe.customers.retrieve(id)` — network-backed.
304
+ * Mirrors `stripe.customers.retrieve({ id })` — network-backed.
248
305
  */
249
- retrieve(id: string, options?: ModelRetrieveOptions): Promise<T | undefined>;
306
+ retrieve(params: ModelRetrieveParams): Promise<T | undefined>;
250
307
  /**
251
308
  * List entities matching a filter from the **server** — async. Same 3-tier
252
309
  * lookup + graph hydration as `retrieve`; single-flight deduped. Returns the
@@ -276,29 +333,36 @@ export interface ModelOperations<T, CreateInput> {
276
333
  * the mutation is queued locally, not when the server confirms.
277
334
  * Server rejection rolls back automatically; watch `sync.syncStatus`.
278
335
  */
279
- create(data: CreateInput, options?: MutationOptions): Promise<T>;
336
+ create(params: ModelCreateParams<T, CreateInput>): Promise<T>;
280
337
  /** Update an entity by id — optimistic, offline-first (see `create`). */
281
- update(id: string, data: Partial<T>, options?: MutationOptions): Promise<T>;
338
+ update(params: ModelUpdateParams<T>): Promise<T>;
282
339
  /** Delete an entity by id — optimistic, offline-first (see `create`). */
283
- delete(id: string, options?: MutationOptions): Promise<void>;
340
+ delete(params: ModelDeleteParams<T>): Promise<void>;
284
341
  /**
285
342
  * Claim a row so other writers wait or are rejected until you're done, and
286
343
  * inspect or manage that coordination through the same namespace. Call it to
287
- * take a claim (the callback form releases when the callback returns or
288
- * throws); reach for its members to observe and steer the wait line:
344
+ * take a claim handle; reach for its members to observe and steer the wait line:
289
345
  *
290
- * - `claim.state(id)` — who holds the row now, or `null` when free
291
- * - `claim.queue(id)` — who's lined up behind the holder
292
- * - `claim.release(id)` — drop a claim early (usually implicit on scope exit)
293
- * - `claim.reorder(id, order)` — re-rank the wait line
346
+ * - `claim.state({ id })` — who holds the row now, or `null` when free
347
+ * - `claim.queue({ id })` — who's lined up behind the holder
348
+ * - `claim.release({ id })` — drop a claim early (usually implicit on scope exit)
349
+ * - `claim.reorder({ id, order })` — re-rank the wait line
294
350
  *
295
351
  * ```ts
296
- * await ablo.weatherReports.claim('report_stockholm', async (report) => {
297
- * const weather = await getWeather(report.location);
298
- * await ablo.weatherReports.update(report.id, { forecast: weather });
352
+ * const claim = await ablo.weatherReports.claim({
353
+ * id: 'report_stockholm',
354
+ * action: 'forecasting',
355
+ * description: 'Fetching fresh weather before updating the report.',
356
+ * });
357
+ * const weather = await getWeather(claim.data.location);
358
+ * await ablo.weatherReports.update({
359
+ * id: claim.target.id,
360
+ * data: { forecast: weather },
361
+ * claim,
299
362
  * });
363
+ * await claim.release();
300
364
  *
301
- * const holder = ablo.weatherReports.claim.state('report_stockholm');
365
+ * const holder = ablo.weatherReports.claim.state({ id: 'report_stockholm' });
302
366
  * ```
303
367
  */
304
368
  claim: ClaimApi<T>;