@abloatai/ablo 0.17.0 → 0.19.0

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 (64) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/BaseSyncedStore.d.ts +5 -4
  3. package/dist/BaseSyncedStore.js +38 -14
  4. package/dist/NetworkMonitor.js +3 -1
  5. package/dist/ObjectPool.d.ts +14 -1
  6. package/dist/ObjectPool.js +8 -1
  7. package/dist/SyncClient.js +7 -1
  8. package/dist/SyncEngineContext.js +2 -0
  9. package/dist/ai-sdk/coordination-context.d.ts +2 -1
  10. package/dist/ai-sdk/coordination-context.js +3 -3
  11. package/dist/ai-sdk/index.d.ts +3 -4
  12. package/dist/ai-sdk/index.js +2 -3
  13. package/dist/ai-sdk/wrap.d.ts +13 -18
  14. package/dist/ai-sdk/wrap.js +2 -6
  15. package/dist/cli.cjs +459 -205
  16. package/dist/client/Ablo.d.ts +116 -23
  17. package/dist/client/Ablo.js +128 -38
  18. package/dist/client/ApiClient.d.ts +2 -2
  19. package/dist/client/ApiClient.js +27 -32
  20. package/dist/client/auth.d.ts +26 -0
  21. package/dist/client/auth.js +209 -1
  22. package/dist/client/createModelProxy.d.ts +16 -11
  23. package/dist/client/createModelProxy.js +15 -15
  24. package/dist/client/sessionMint.d.ts +10 -0
  25. package/dist/client/sessionMint.js +8 -1
  26. package/dist/coordination/schema.d.ts +10 -10
  27. package/dist/coordination/schema.js +7 -8
  28. package/dist/coordination/trace.d.ts +91 -0
  29. package/dist/coordination/trace.js +147 -0
  30. package/dist/core/StoreManager.js +3 -1
  31. package/dist/errorCodes.d.ts +2 -0
  32. package/dist/errorCodes.js +2 -0
  33. package/dist/errors.d.ts +10 -2
  34. package/dist/errors.js +20 -9
  35. package/dist/index.d.ts +7 -1
  36. package/dist/index.js +12 -0
  37. package/dist/interfaces/index.d.ts +45 -2
  38. package/dist/policy/types.d.ts +33 -9
  39. package/dist/policy/types.js +42 -11
  40. package/dist/react/AbloProvider.d.ts +2 -2
  41. package/dist/schema/ddl.d.ts +36 -0
  42. package/dist/schema/ddl.js +66 -0
  43. package/dist/schema/index.d.ts +1 -1
  44. package/dist/schema/index.js +1 -1
  45. package/dist/surface.d.ts +1 -1
  46. package/dist/surface.js +2 -0
  47. package/dist/sync/HydrationCoordinator.js +7 -3
  48. package/dist/sync/SyncWebSocket.d.ts +11 -2
  49. package/dist/sync/SyncWebSocket.js +68 -4
  50. package/dist/sync/awaitClaimGrant.js +9 -2
  51. package/dist/sync/createClaimStream.d.ts +9 -2
  52. package/dist/sync/createClaimStream.js +41 -7
  53. package/dist/sync/participants.d.ts +12 -11
  54. package/dist/sync/participants.js +5 -5
  55. package/dist/transactions/TransactionQueue.d.ts +1 -0
  56. package/dist/transactions/TransactionQueue.js +40 -3
  57. package/dist/types/streams.d.ts +57 -135
  58. package/dist/wire/errorEnvelope.d.ts +13 -0
  59. package/docs/coordination.md +16 -0
  60. package/docs/debugging.md +194 -0
  61. package/docs/index.md +1 -0
  62. package/package.json +1 -1
  63. package/dist/ai-sdk/claim-broadcast.d.ts +0 -83
  64. package/dist/ai-sdk/claim-broadcast.js +0 -79
@@ -63,7 +63,14 @@ export async function mintSession(params, ctx) {
63
63
  userMeta: params.userMeta ?? { id: res.participantId },
64
64
  };
65
65
  }
66
- const operations = Object.entries(params.can).flatMap(([model, ops]) => (ops ?? []).map((op) => `${model.toLowerCase()}.${op}`));
66
+ const operations = Object.entries(params.can).flatMap(([model, ops]) => {
67
+ // Translate the schema key the developer used in `can` to the wire
68
+ // typename the Hub gates on — see `modelTypenames` above. Falls back to
69
+ // the key when no map is supplied (schemaless client) or the key isn't
70
+ // in it, preserving the prior behaviour exactly for those callers.
71
+ const ns = ctx.modelTypenames?.[model] ?? model;
72
+ return (ops ?? []).map((op) => `${ns.toLowerCase()}.${op}`);
73
+ });
67
74
  const res = await exchangeApiKey({
68
75
  apiKey,
69
76
  baseUrl,
@@ -209,7 +209,7 @@ export declare const wireClaimSummarySchema: z.ZodObject<{
209
209
  entityId: z.ZodString;
210
210
  meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
211
211
  claimId: z.ZodString;
212
- action: z.ZodString;
212
+ reason: z.ZodString;
213
213
  declaredAt: z.ZodNumber;
214
214
  expiresAt: z.ZodNumber;
215
215
  }, z.core.$strip>;
@@ -227,7 +227,7 @@ export declare const claimErrorSchema: z.ZodObject<{
227
227
  entityId: z.ZodString;
228
228
  meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
229
229
  claimId: z.ZodString;
230
- action: z.ZodString;
230
+ reason: z.ZodString;
231
231
  declaredAt: z.ZodNumber;
232
232
  expiresAt: z.ZodNumber;
233
233
  }, z.core.$strip>>;
@@ -237,7 +237,7 @@ export type ClaimError = z.infer<typeof claimErrorSchema>;
237
237
  /**
238
238
  * A declared pending-mutation claim — the unit broadcast in presence
239
239
  * `activeClaims`. Clients supply the descriptive `targetRef` fields, an
240
- * `action`, and a chosen `claimId`; the SERVER stamps `declaredAt` /
240
+ * explanatory `reason`, and a chosen `claimId`; the SERVER stamps `declaredAt` /
241
241
  * `expiresAt` and may set `status` / `error`.
242
242
  *
243
243
  * `status` and `error` are OPTIONAL: this single shape serves both the
@@ -259,7 +259,7 @@ export declare const wireClaimSchema: z.ZodObject<{
259
259
  field: z.ZodOptional<z.ZodString>;
260
260
  meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
261
261
  claimId: z.ZodString;
262
- action: z.ZodString;
262
+ reason: z.ZodString;
263
263
  declaredAt: z.ZodNumber;
264
264
  expiresAt: z.ZodNumber;
265
265
  status: z.ZodOptional<z.ZodEnum<{
@@ -280,7 +280,7 @@ export declare const wireClaimSchema: z.ZodObject<{
280
280
  entityId: z.ZodString;
281
281
  meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
282
282
  claimId: z.ZodString;
283
- action: z.ZodString;
283
+ reason: z.ZodString;
284
284
  declaredAt: z.ZodNumber;
285
285
  expiresAt: z.ZodNumber;
286
286
  }, z.core.$strip>>;
@@ -313,7 +313,7 @@ export declare const claimRejectionSchema: z.ZodObject<{
313
313
  entityId: z.ZodString;
314
314
  meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
315
315
  claimId: z.ZodString;
316
- action: z.ZodString;
316
+ reason: z.ZodString;
317
317
  declaredAt: z.ZodNumber;
318
318
  expiresAt: z.ZodNumber;
319
319
  }, z.core.$strip>>;
@@ -385,7 +385,7 @@ export declare const modelClaimSchema: z.ZodReadonly<z.ZodObject<{
385
385
  }, z.core.$strip>>;
386
386
  export type ModelClaim = z.infer<typeof modelClaimSchema>;
387
387
  /**
388
- * `claim_begin` payload (client → server). The descriptive target + action,
388
+ * `claim_begin` payload (client → server). The descriptive target + reason,
389
389
  * plus an optional duration hint and the opt-in fair-queue flag. The server
390
390
  * stamps the lifecycle/timestamp fields, so they are NOT part of the inbound
391
391
  * shape — this is exactly what the WS ingest validates.
@@ -403,7 +403,7 @@ export declare const claimBeginPayloadSchema: z.ZodObject<{
403
403
  field: z.ZodOptional<z.ZodString>;
404
404
  meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
405
405
  claimId: z.ZodString;
406
- action: z.ZodString;
406
+ reason: z.ZodString;
407
407
  estimatedMs: z.ZodOptional<z.ZodNumber>;
408
408
  queue: z.ZodOptional<z.ZodBoolean>;
409
409
  }, z.core.$strip>;
@@ -585,7 +585,7 @@ export declare const presenceUpdateFrameSchema: z.ZodObject<{
585
585
  field: z.ZodOptional<z.ZodString>;
586
586
  meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
587
587
  claimId: z.ZodString;
588
- action: z.ZodString;
588
+ reason: z.ZodString;
589
589
  declaredAt: z.ZodNumber;
590
590
  expiresAt: z.ZodNumber;
591
591
  status: z.ZodOptional<z.ZodEnum<{
@@ -606,7 +606,7 @@ export declare const presenceUpdateFrameSchema: z.ZodObject<{
606
606
  entityId: z.ZodString;
607
607
  meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
608
608
  claimId: z.ZodString;
609
- action: z.ZodString;
609
+ reason: z.ZodString;
610
610
  declaredAt: z.ZodNumber;
611
611
  expiresAt: z.ZodNumber;
612
612
  }, z.core.$strip>>;
@@ -209,8 +209,8 @@ export const claimStatusSchema = z.enum([
209
209
  ]);
210
210
  const wireClaimBaseSchema = targetRefSchema.extend({
211
211
  claimId: z.string(),
212
- /** Verb the agent expects: 'update' | 'create' | 'editing' | 'reviewing' … */
213
- action: z.string(),
212
+ /** Human-readable phase: 'editing' | 'reviewing' | 'forecasting' … */
213
+ reason: z.string(),
214
214
  /** Server-stamped declaration time (epoch ms). */
215
215
  declaredAt: z.number(),
216
216
  /** Server-computed TTL deadline (epoch ms). Readers treat as advisory. */
@@ -219,7 +219,7 @@ const wireClaimBaseSchema = targetRefSchema.extend({
219
219
  });
220
220
  export const wireClaimSummarySchema = wireClaimBaseSchema.pick({
221
221
  claimId: true,
222
- action: true,
222
+ reason: true,
223
223
  declaredAt: true,
224
224
  expiresAt: true,
225
225
  entityType: true,
@@ -243,7 +243,7 @@ export const claimErrorSchema = z.object({
243
243
  /**
244
244
  * A declared pending-mutation claim — the unit broadcast in presence
245
245
  * `activeClaims`. Clients supply the descriptive `targetRef` fields, an
246
- * `action`, and a chosen `claimId`; the SERVER stamps `declaredAt` /
246
+ * explanatory `reason`, and a chosen `claimId`; the SERVER stamps `declaredAt` /
247
247
  * `expiresAt` and may set `status` / `error`.
248
248
  *
249
249
  * `status` and `error` are OPTIONAL: this single shape serves both the
@@ -298,8 +298,7 @@ export const modelClaimSchema = z
298
298
  id: z.string(),
299
299
  actor: z.string(),
300
300
  participantKind: wireParticipantKindSchema,
301
- /** Human-readable phase (`'editing'`). The public SDK field; the WS/HTTP
302
- * wire carries the same value as `action` (healed on read). */
301
+ /** Human-readable phase (`'editing'`). */
303
302
  reason: z.string(),
304
303
  description: z.string().optional(),
305
304
  field: z.string().optional(),
@@ -310,14 +309,14 @@ export const modelClaimSchema = z
310
309
  })
311
310
  .readonly();
312
311
  /**
313
- * `claim_begin` payload (client → server). The descriptive target + action,
312
+ * `claim_begin` payload (client → server). The descriptive target + reason,
314
313
  * plus an optional duration hint and the opt-in fair-queue flag. The server
315
314
  * stamps the lifecycle/timestamp fields, so they are NOT part of the inbound
316
315
  * shape — this is exactly what the WS ingest validates.
317
316
  */
318
317
  export const claimBeginPayloadSchema = targetRefSchema.extend({
319
318
  claimId: z.string(),
320
- action: z.string(),
319
+ reason: z.string(),
321
320
  /** Hint for `expiresAt`; the server caps it. */
322
321
  estimatedMs: z.number().optional(),
323
322
  /**
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Claim log — the easy way to SEE agents colliding.
3
+ *
4
+ * The engine emits the claim lifecycle (`acquired → queued → granted → lost /
5
+ * rejected / expired`) and the notify-instead-of-abort stale-write collision
6
+ * through two seams: human `logger` lines (visible with `new Ablo({ debug: true })`)
7
+ * and structured `observability.captureClaim` / `captureConflict` calls.
8
+ *
9
+ * This is the third, evals-shaped path: hand a {@link ClaimLog} to
10
+ * `Ablo({ observability })`, run your scenario, then read back an ordered list
11
+ * you can print for eyeballing or `collisions()` for assertions.
12
+ */
13
+ import type { SyncObservabilityProvider, ClaimEvent, ConflictEvent } from '../interfaces/index.js';
14
+ /** A claim state change as one quiet, greppable line. */
15
+ export declare function formatClaim(e: ClaimEvent): string;
16
+ /** A notify-instead-of-abort stale write as one readable line. */
17
+ export declare function formatConflict(e: ConflictEvent): string;
18
+ /** One ordered entry in a {@link ClaimLog}. */
19
+ export interface ClaimLogEntry {
20
+ /** Monotonic order index — deterministic, clock-free, eval-friendly. */
21
+ readonly seq: number;
22
+ /** The same one-line text the console and breadcrumb seams emit. */
23
+ readonly line: string;
24
+ /** A collision worth flagging: a rejected/lost claim or a stale write. */
25
+ readonly collision: boolean;
26
+ readonly claim?: ClaimEvent;
27
+ readonly conflict?: ConflictEvent;
28
+ }
29
+ /**
30
+ * Collects the claim lifecycle + stale-write collisions into an ordered list.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * const log = new ClaimLog();
35
+ * const ablo = new Ablo({ schema, apiKey, observability: log });
36
+ * // …run the agents…
37
+ * console.log(`${log}`); // pretty timeline for eyeballing
38
+ * expect(log.collisions()).toHaveLength(0); // assert no one stepped on anyone
39
+ * ```
40
+ *
41
+ * Implements the full {@link SyncObservabilityProvider} (every method it does not
42
+ * care about is an inert no-op), so it drops straight into the `observability`
43
+ * slot with no adapter.
44
+ */
45
+ export declare class ClaimLog implements SyncObservabilityProvider {
46
+ private readonly max;
47
+ private rows;
48
+ private seq;
49
+ private readonly listeners;
50
+ /** Cap the buffer so a long-running session can't grow unbounded. */
51
+ constructor(max?: number);
52
+ captureClaim(claim: ClaimEvent): void;
53
+ captureConflict(conflict: ConflictEvent): void;
54
+ /**
55
+ * Fire `listener` every time an event lands. Returns an unsubscribe fn. Same
56
+ * contract as `ablo.claims.onChange` — drop it into `useSyncExternalStore` in
57
+ * React or `autorun` in MobX to render a live activity feed.
58
+ *
59
+ * @example
60
+ * ```tsx
61
+ * const log = useMemo(() => new ClaimLog(), []);
62
+ * const entries = useSyncExternalStore(log.onChange, () => log.entries);
63
+ * return <ul>{entries.map((e) => <li key={e.seq}>{e.line}</li>)}</ul>;
64
+ * ```
65
+ */
66
+ readonly onChange: (listener: () => void) => (() => void);
67
+ /** The full ordered list. Stable reference until the next event lands. */
68
+ get entries(): readonly ClaimLogEntry[];
69
+ /** Only the collisions: rejected/lost claims + stale writes. */
70
+ collisions(): readonly ClaimLogEntry[];
71
+ /** Drop everything — useful between scenarios in one process. */
72
+ clear(): void;
73
+ /** Printable: one event per line, collisions marked. */
74
+ toString(): string;
75
+ private add;
76
+ private notify;
77
+ setContext(): void;
78
+ setConnectionState(): void;
79
+ breadcrumb(): void;
80
+ captureRollback(): void;
81
+ captureTransactionFailure(): void;
82
+ captureBootstrapFailure(): void;
83
+ captureReconciliation(): void;
84
+ captureDeltaRetryExhausted(): void;
85
+ captureWebSocketError(): void;
86
+ captureOfflineFlushFailure(): void;
87
+ captureSelfHealing(): void;
88
+ captureCommitZeroSyncId(): void;
89
+ startSpan<T>(_name: string, _op: string, fn: () => T): T;
90
+ startSpanAsync<T>(_name: string, _op: string, fn: () => Promise<T>): Promise<T>;
91
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Claim log — the easy way to SEE agents colliding.
3
+ *
4
+ * The engine emits the claim lifecycle (`acquired → queued → granted → lost /
5
+ * rejected / expired`) and the notify-instead-of-abort stale-write collision
6
+ * through two seams: human `logger` lines (visible with `new Ablo({ debug: true })`)
7
+ * and structured `observability.captureClaim` / `captureConflict` calls.
8
+ *
9
+ * This is the third, evals-shaped path: hand a {@link ClaimLog} to
10
+ * `Ablo({ observability })`, run your scenario, then read back an ordered list
11
+ * you can print for eyeballing or `collisions()` for assertions.
12
+ */
13
+ // ─────────────────────────────────────────────
14
+ // Formatters — one readable line per event. Shared by the WS logger seam and
15
+ // the log so console output and log output never drift.
16
+ // ─────────────────────────────────────────────
17
+ /** A claim state change as one quiet, greppable line. */
18
+ export function formatClaim(e) {
19
+ const target = e.model && e.id ? `${e.model}/${e.id}` : e.claimId ?? 'unknown target';
20
+ const scope = e.field ? `#${e.field}` : '';
21
+ const pos = e.position !== undefined ? ` [pos ${e.position}]` : '';
22
+ const actor = e.actor
23
+ ? `${e.actor}${e.participantKind ? ` (${e.participantKind})` : ''}`
24
+ : '';
25
+ // `rejected`/`lost` name the BLOCKING holder; the rest name us (or no one).
26
+ const by = actor
27
+ ? e.phase === 'rejected' || e.phase === 'lost'
28
+ ? ` — held by ${actor}`
29
+ : ` — by ${actor}`
30
+ : '';
31
+ const why = e.reason ? `: ${e.reason}` : '';
32
+ return `claim ${e.phase}: ${target}${scope}${pos}${by}${why}`;
33
+ }
34
+ /** A notify-instead-of-abort stale write as one readable line. */
35
+ export function formatConflict(e) {
36
+ const rows = e.rows.map((r) => `${r.model}/${r.id}(${r.fields.join(',')})`).join(', ');
37
+ return `conflict: tx ${e.clientTxId} — ${e.rows.length} row(s) changed underneath${rows ? `: ${rows}` : ''}`;
38
+ }
39
+ /**
40
+ * Collects the claim lifecycle + stale-write collisions into an ordered list.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * const log = new ClaimLog();
45
+ * const ablo = new Ablo({ schema, apiKey, observability: log });
46
+ * // …run the agents…
47
+ * console.log(`${log}`); // pretty timeline for eyeballing
48
+ * expect(log.collisions()).toHaveLength(0); // assert no one stepped on anyone
49
+ * ```
50
+ *
51
+ * Implements the full {@link SyncObservabilityProvider} (every method it does not
52
+ * care about is an inert no-op), so it drops straight into the `observability`
53
+ * slot with no adapter.
54
+ */
55
+ export class ClaimLog {
56
+ max;
57
+ // Immutable list: a NEW array reference on every change. This is what lets
58
+ // `useSyncExternalStore(log.onChange, () => log.entries)` detect updates —
59
+ // it compares snapshots by reference, so an in-place push would never render.
60
+ rows = [];
61
+ seq = 0;
62
+ listeners = new Set();
63
+ /** Cap the buffer so a long-running session can't grow unbounded. */
64
+ constructor(max = 1_000) {
65
+ this.max = max;
66
+ }
67
+ // —— the two seams we record ——
68
+ captureClaim(claim) {
69
+ this.add({
70
+ line: formatClaim(claim),
71
+ collision: claim.phase === 'rejected' || claim.phase === 'lost',
72
+ claim,
73
+ });
74
+ }
75
+ captureConflict(conflict) {
76
+ this.add({ line: formatConflict(conflict), collision: true, conflict });
77
+ }
78
+ // —— reactivity ——
79
+ /**
80
+ * Fire `listener` every time an event lands. Returns an unsubscribe fn. Same
81
+ * contract as `ablo.claims.onChange` — drop it into `useSyncExternalStore` in
82
+ * React or `autorun` in MobX to render a live activity feed.
83
+ *
84
+ * @example
85
+ * ```tsx
86
+ * const log = useMemo(() => new ClaimLog(), []);
87
+ * const entries = useSyncExternalStore(log.onChange, () => log.entries);
88
+ * return <ul>{entries.map((e) => <li key={e.seq}>{e.line}</li>)}</ul>;
89
+ * ```
90
+ */
91
+ onChange = (listener) => {
92
+ this.listeners.add(listener);
93
+ return () => this.listeners.delete(listener);
94
+ };
95
+ // —— inspection ——
96
+ /** The full ordered list. Stable reference until the next event lands. */
97
+ get entries() {
98
+ return this.rows;
99
+ }
100
+ /** Only the collisions: rejected/lost claims + stale writes. */
101
+ collisions() {
102
+ return this.rows.filter((e) => e.collision);
103
+ }
104
+ /** Drop everything — useful between scenarios in one process. */
105
+ clear() {
106
+ this.rows = [];
107
+ this.seq = 0;
108
+ this.notify();
109
+ }
110
+ /** Printable: one event per line, collisions marked. */
111
+ toString() {
112
+ if (this.rows.length === 0)
113
+ return 'claim log: (empty)';
114
+ const w = String(this.rows.length).length;
115
+ return this.rows
116
+ .map((e) => `${String(e.seq).padStart(w)} ${e.collision ? '⚠ ' : ' '}${e.line}`)
117
+ .join('\n');
118
+ }
119
+ add(entry) {
120
+ const next = [...this.rows, { seq: this.seq++, ...entry }];
121
+ this.rows = next.length > this.max ? next.slice(next.length - this.max) : next;
122
+ this.notify();
123
+ }
124
+ notify() {
125
+ for (const listener of this.listeners)
126
+ listener();
127
+ }
128
+ // —— inert provider surface (this log only cares about claims) ——
129
+ setContext() { }
130
+ setConnectionState() { }
131
+ breadcrumb() { }
132
+ captureRollback() { }
133
+ captureTransactionFailure() { }
134
+ captureBootstrapFailure() { }
135
+ captureReconciliation() { }
136
+ captureDeltaRetryExhausted() { }
137
+ captureWebSocketError() { }
138
+ captureOfflineFlushFailure() { }
139
+ captureSelfHealing() { }
140
+ captureCommitZeroSyncId() { }
141
+ startSpan(_name, _op, fn) {
142
+ return fn();
143
+ }
144
+ startSpanAsync(_name, _op, fn) {
145
+ return fn();
146
+ }
147
+ }
@@ -274,7 +274,9 @@ export class StoreManager {
274
274
  * Clear all stores
275
275
  */
276
276
  async clearAllStores() {
277
- getContext().logger.warn('Clearing all stores');
277
+ // Lifecycle chatter (logout / identity switch / reset) NOT a warning.
278
+ // `debug` so it's silent under the default `warn` threshold.
279
+ getContext().logger.debug('Clearing all stores');
278
280
  const promises = Array.from(this.stores.values()).map((store) => store.clear());
279
281
  await Promise.all(promises);
280
282
  getContext().logger.info('All stores cleared');
@@ -135,6 +135,7 @@ export declare const ERROR_CODES: {
135
135
  readonly browser_database_url_blocked: ErrorCodeSpec;
136
136
  readonly datasource_registration_failed: ErrorCodeSpec;
137
137
  readonly datasource_connection_unsupported: ErrorCodeSpec;
138
+ readonly datasource_direct_deprecated: ErrorCodeSpec;
138
139
  readonly capability_scope_denied: ErrorCodeSpec;
139
140
  readonly issuer_register_forbidden: ErrorCodeSpec;
140
141
  readonly capability_invalid: ErrorCodeSpec;
@@ -188,6 +189,7 @@ export declare const ERROR_CODES: {
188
189
  readonly unique_violation: ErrorCodeSpec;
189
190
  readonly check_violation: ErrorCodeSpec;
190
191
  readonly constraint_violation: ErrorCodeSpec;
192
+ readonly column_type_mismatch: ErrorCodeSpec;
191
193
  readonly server_execute_unknown_model: ErrorCodeSpec;
192
194
  readonly mutate_create_unknown_model: ErrorCodeSpec;
193
195
  readonly tenant_model_columns_unknown: ErrorCodeSpec;
@@ -125,6 +125,7 @@ export const ERROR_CODES = {
125
125
  browser_database_url_blocked: client('auth', 'A database connection string must not be used from a browser context — it carries DB credentials.'),
126
126
  datasource_registration_failed: client('auth', 'Failed to register the provided databaseUrl as a datasource.'),
127
127
  datasource_connection_unsupported: wire('validation', 400, false, 'This deployment cannot register a direct (connection string) datasource — use the signed endpoint kind.'),
128
+ datasource_direct_deprecated: wire('validation', 410, false, 'The direct (connection string) datasource is deprecated. Register a signed Data Source endpoint instead — your app owns the write and your credentials never leave it.'),
128
129
  // ── permission / capability (403) ──────────────────────────────────
129
130
  capability_scope_denied: wire('capability', 403, false, "The connection's resolved scope does not cover the attempted action."),
130
131
  issuer_register_forbidden: wire('permission', 403, false, 'Registering a trusted issuer requires a secret (sk_) API key.'),
@@ -201,6 +202,7 @@ export const ERROR_CODES = {
201
202
  unique_violation: wire('conflict', 409, false, 'A value violates a uniqueness constraint.'),
202
203
  check_violation: wire('validation', 400, false, 'A value violates a database check constraint.'),
203
204
  constraint_violation: wire('validation', 400, false, 'A database integrity constraint was violated.'),
205
+ column_type_mismatch: wire('validation', 400, false, 'A structured (JSON) value was written to a column whose database type is not jsonb. The column was not provisioned as jsonb (often a pre-existing column adopted by `ablo push`); storing the value would silently corrupt it to "[object Object]". Run `ablo migrate` to alter the column to jsonb.'),
204
206
  // ── tenant / unknown model (400) ───────────────────────────────────
205
207
  server_execute_unknown_model: wire('tenant', 400, false, 'Wrote to a model the server does not know. The server keeps its own copy of the schema — run `ablo push` (or keep `ablo dev` running) to upload `ablo/schema.ts` before writing to new or changed models.'),
206
208
  mutate_create_unknown_model: wire('tenant', 400, false, 'Created a model the server does not know. Run `ablo push` (or keep `ablo dev` running) to upload `ablo/schema.ts` first — the server keeps its own copy of the schema.'),
package/dist/errors.d.ts CHANGED
@@ -77,6 +77,15 @@ export declare class AbloError extends Error {
77
77
  request_id?: string;
78
78
  [key: string]: unknown;
79
79
  };
80
+ /**
81
+ * A single, leak-proof line for logs and `String(err)` / template
82
+ * interpolation: `AbloValidationError [code]: message (see docs) [request_id: …]`.
83
+ *
84
+ * Deliberately does NOT dump `details`, `cause`, or the stack — the thing that
85
+ * makes `console.error(richError)` an unreadable wall of text. The structured
86
+ * payload stays available via {@link toJSON}; this is the human one-liner.
87
+ */
88
+ toString(): string;
80
89
  }
81
90
  /**
82
91
  * Map a stable error `code` to its docs URL — the one place the convention
@@ -176,8 +185,7 @@ export interface ClaimContext {
176
185
  readonly claimId?: string;
177
186
  readonly actor?: string;
178
187
  readonly participantKind?: ParticipantKind;
179
- /** Human-readable phase the holder is in (`'editing'`). Matches the public
180
- * claim surface; the wire summary carries the same value as `action`. */
188
+ /** Human-readable phase the holder is in (`'editing'`). */
181
189
  readonly reason?: string;
182
190
  readonly description?: string;
183
191
  readonly field?: string;
package/dist/errors.js CHANGED
@@ -91,6 +91,20 @@ export class AbloError extends Error {
91
91
  ...(this.details ?? {}),
92
92
  };
93
93
  }
94
+ /**
95
+ * A single, leak-proof line for logs and `String(err)` / template
96
+ * interpolation: `AbloValidationError [code]: message (see docs) [request_id: …]`.
97
+ *
98
+ * Deliberately does NOT dump `details`, `cause`, or the stack — the thing that
99
+ * makes `console.error(richError)` an unreadable wall of text. The structured
100
+ * payload stays available via {@link toJSON}; this is the human one-liner.
101
+ */
102
+ toString() {
103
+ const code = this.code ? ` [${this.code}]` : '';
104
+ const docs = this.docUrl ? ` (see ${this.docUrl})` : '';
105
+ const req = this.requestId ? ` [request_id: ${this.requestId}]` : '';
106
+ return `${this.name}${code}: ${this.message}${docs}${req}`;
107
+ }
94
108
  }
95
109
  /**
96
110
  * Map a stable error `code` to its docs URL — the one place the convention
@@ -182,13 +196,10 @@ export class AbloStaleContextError extends AbloError {
182
196
  this.conflicts = options.conflicts;
183
197
  }
184
198
  }
185
- function claimAction(claim) {
199
+ function claimReason(claim) {
186
200
  if (!claim)
187
201
  return undefined;
188
- // The public `ClaimContext` exposes the phase as `reason`; the wire
189
- // `WireClaimSummary` projection still carries it under `action`. Read both.
190
- const c = claim;
191
- return c.reason ?? c.action;
202
+ return claim.reason;
192
203
  }
193
204
  function claimDescription(claim) {
194
205
  if (!claim)
@@ -215,20 +226,20 @@ function secondsUntil(ms, now = Date.now()) {
215
226
  }
216
227
  export function formatClaimedErrorMessage(args) {
217
228
  const holder = claimActor(args.claim, args.heldBy);
218
- const action = claimAction(args.claim);
229
+ const reason = claimReason(args.claim);
219
230
  const description = claimDescription(args.claim);
220
231
  const expiresIn = secondsUntil(claimExpiresAt(args.claim));
221
- if (!holder && !action && !description) {
232
+ if (!holder && !reason && !description) {
222
233
  return args.fallback ?? `Model row is claimed: ${args.targetLabel}.`;
223
234
  }
224
235
  const actor = holder ?? 'another participant';
225
- const actionPart = action ? ` (${action})` : '';
236
+ const reasonPart = reason ? ` (${reason})` : '';
226
237
  const descriptionPart = description ? `: ${description}` : '';
227
238
  const expiresPart = expiresIn !== undefined ? ` - expires in ${expiresIn}s` : '';
228
239
  const policyPart = args.policyReason
229
240
  ? ` Policy reason: ${args.policyReason}.`
230
241
  : '';
231
- return `Claimed by ${actor}${actionPart}${descriptionPart}${expiresPart} on ${args.targetLabel}.${policyPart}`;
242
+ return `Claimed by ${actor}${reasonPart}${descriptionPart}${expiresPart} on ${args.targetLabel}.${policyPart}`;
232
243
  }
233
244
  /**
234
245
  * The target entity is currently claimed by another participant and the caller
package/dist/index.d.ts CHANGED
@@ -52,7 +52,7 @@ export type { MutationExecutor } from './interfaces/index.js';
52
52
  export type { HttpClaimApi, InternalAbloOptions } from './client/Ablo.js';
53
53
  export { type AbloHttpClientOptions, type AbloHttpClient, type HttpModelClient, } from './client/httpClient.js';
54
54
  export { ABLO_DEFAULT_BASE_URL, ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_URL, normalizeAbloHostedBaseUrl, } from './client/auth.js';
55
- export type { AbloOptions, LocalCountOptions, LocalReadOptions, ModelListScope, ServerReadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, ClaimHandle, ModelOperations, } from './client/Ablo.js';
55
+ export type { AbloOptions, LocalCountOptions, LocalReadOptions, ModelListScope, ServerReadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, Claim, ModelOperations, } from './client/Ablo.js';
56
56
  export type { AbloPersistence } from './client/persistence.js';
57
57
  import { Ablo } from './client/Ablo.js';
58
58
  export default Ablo;
@@ -62,6 +62,8 @@ export { defaultPolicy, capabilityPreemptPolicy, interpretConflictAxis } from '.
62
62
  export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloNotFoundError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, hasWireCode, errorFromWire, toAbloError, ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode, classifyRecovery, recoveryClassSchema, RECOVERY_CLASSES, } from './errors.js';
63
63
  export type { CommitReceipt, RequiredCapability } from './errors.js';
64
64
  export type { ErrorCode, WireErrorCode, ErrorCategory, ErrorCodeSpec, RecoveryClass } from './errors.js';
65
+ export { errorEnvelope, statusForType } from './wire/errorEnvelope.js';
66
+ export type { ErrorEnvelope } from './wire/errorEnvelope.js';
65
67
  export { WS_BEARER_SUBPROTOCOL_PREFIX, WS_SYNC_SUBPROTOCOL } from './auth/credentialSource.js';
66
68
  export { ENVIRONMENTS, environmentSchema, normalizeEnvironment, environmentFromKeyPrefix, environmentToKeyPrefix, isSandboxEnvironment, } from './environment.js';
67
69
  export type { Environment, KeyPrefixEnvironment } from './environment.js';
@@ -70,6 +72,10 @@ export type { WriteOptionsInput } from './client/writeOptionsSchema.js';
70
72
  export type { WriteOptions, MutationOptions } from './interfaces/index.js';
71
73
  export { staleNotificationSchema, readDependencySchema } from './coordination/schema.js';
72
74
  export type { StaleNotification, ReadDependency } from './coordination/schema.js';
75
+ export { ClaimLog, formatClaim, formatConflict } from './coordination/trace.js';
76
+ export type { ClaimLogEntry } from './coordination/trace.js';
77
+ export type { ClaimEvent, ConflictEvent, SyncObservabilityProvider, } from './interfaces/index.js';
78
+ export { noopObservability } from './SyncEngineContext.js';
73
79
  export { IDBOpenTimeoutError, isStorageOpenTimeout } from './core/openIDBWithTimeout.js';
74
80
  export { PUBLIC_MODEL_VERBS, PUBLIC_LIST_OPTION_KEYS, PUBLIC_ABLO_OPTION_KEYS, } from './surface.js';
75
81
  export type { ModelVerb, ListOptionKey, AbloOptionKey } from './surface.js';
package/dist/index.js CHANGED
@@ -85,6 +85,11 @@ export { defaultPolicy, capabilityPreemptPolicy, interpretConflictAxis } from '.
85
85
  // consumers need to discriminate failures (`e instanceof AbloX` or
86
86
  // `e.type === 'AbloX'`) plus the HTTP-response translator.
87
87
  export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloNotFoundError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, hasWireCode, errorFromWire, toAbloError, ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode, classifyRecovery, recoveryClassSchema, RECOVERY_CLASSES, } from './errors.js';
88
+ // Canonical wire-egress contract (dependency-free): the error envelope shape +
89
+ // the AbloError-subclass→HTTP-status table. Re-exported so server consumers
90
+ // (e.g. apps/sync-server, which keeps its own self-contained copy) can assert
91
+ // against the ONE source instead of silently drifting. See wire/errorEnvelope.ts.
92
+ export { errorEnvelope, statusForType } from './wire/errorEnvelope.js';
88
93
  export { WS_BEARER_SUBPROTOCOL_PREFIX, WS_SYNC_SUBPROTOCOL } from './auth/credentialSource.js';
89
94
  export { ENVIRONMENTS, environmentSchema, normalizeEnvironment, environmentFromKeyPrefix, environmentToKeyPrefix, isSandboxEnvironment, } from './environment.js';
90
95
  // THE write-options contract — the one Zod schema for the option bag every
@@ -99,6 +104,13 @@ export { writeOptionsSchema, onStaleModeSchema, assertWriteOptions, } from './cl
99
104
  // agent can self-heal rather than discard work (see coordination/schema.ts and
100
105
  // docs/coordination.md → "Notify, do not abort").
101
106
  export { staleNotificationSchema, readDependencySchema } from './coordination/schema.js';
107
+ // Claim log — collect claim events + stale-write collisions into an ordered list
108
+ // you can print for eyeballing or collisions() for evals assertions. Hand
109
+ // `new ClaimLog()` to `Ablo({ observability })`.
110
+ export { ClaimLog, formatClaim, formatConflict } from './coordination/trace.js';
111
+ // Spread this to provide a custom `observability` that overrides only the hooks
112
+ // you care about (e.g. captureClaim) and no-ops the rest.
113
+ export { noopObservability } from './SyncEngineContext.js';
102
114
  // Storage-wedge detection — lets app shells render a recovery screen when the
103
115
  // IndexedDB backing store is stuck (see core/openIDBWithTimeout.ts).
104
116
  export { IDBOpenTimeoutError, isStorageOpenTimeout } from './core/openIDBWithTimeout.js';