@abloatai/transaction 0.59.1 → 0.60.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 (166) hide show
  1. package/dist/auth/apiKey.d.ts +10 -11
  2. package/dist/auth/apiKey.d.ts.map +1 -1
  3. package/dist/auth/apiKey.js +15 -29
  4. package/dist/auth/apiKey.js.map +1 -1
  5. package/dist/auth/browserCredentialSafety.js +1 -1
  6. package/dist/auth/browserCredentialSafety.js.map +1 -1
  7. package/dist/auth/capabilityLifecycle.d.ts +1 -1
  8. package/dist/auth/capabilityLifecycle.d.ts.map +1 -1
  9. package/dist/auth/capabilityLifecycle.js +2 -1
  10. package/dist/auth/capabilityLifecycle.js.map +1 -1
  11. package/dist/auth/credentialEndpoint.js +1 -1
  12. package/dist/auth/credentialEndpoint.js.map +1 -1
  13. package/dist/auth/credentialResult.d.ts +2 -1
  14. package/dist/auth/credentialResult.d.ts.map +1 -1
  15. package/dist/auth/credentialResult.js.map +1 -1
  16. package/dist/claims/events.d.ts +1 -1
  17. package/dist/claims/events.d.ts.map +1 -1
  18. package/dist/client/ablo.d.ts +15 -11
  19. package/dist/client/ablo.d.ts.map +1 -1
  20. package/dist/client/ablo.js +5 -14
  21. package/dist/client/ablo.js.map +1 -1
  22. package/dist/client/resources/httpResources.d.ts +1 -169
  23. package/dist/client/resources/httpResources.d.ts.map +1 -1
  24. package/dist/client/resources/httpResources.js +1 -1
  25. package/dist/client/resources/httpResources.js.map +1 -1
  26. package/dist/client/resources/modelOperations.d.ts +0 -9
  27. package/dist/client/resources/modelOperations.d.ts.map +1 -1
  28. package/dist/client/surface.d.ts +2 -2
  29. package/dist/client/surface.d.ts.map +1 -1
  30. package/dist/client/surface.js +8 -3
  31. package/dist/client/surface.js.map +1 -1
  32. package/dist/commit/contract.d.ts +10 -10
  33. package/dist/commit/recordRuntime.d.ts +3 -2
  34. package/dist/commit/recordRuntime.d.ts.map +1 -1
  35. package/dist/commit/recordRuntime.js +1 -0
  36. package/dist/commit/recordRuntime.js.map +1 -1
  37. package/dist/coordination/index.d.ts +2 -2
  38. package/dist/coordination/index.d.ts.map +1 -1
  39. package/dist/coordination/index.js +3 -4
  40. package/dist/coordination/index.js.map +1 -1
  41. package/dist/coordination/schema.d.ts +3 -47
  42. package/dist/coordination/schema.d.ts.map +1 -1
  43. package/dist/coordination/schema.js +7 -69
  44. package/dist/coordination/schema.js.map +1 -1
  45. package/dist/errorCodes.d.ts +2 -2
  46. package/dist/errorCodes.d.ts.map +1 -1
  47. package/dist/errorCodes.js +5 -9
  48. package/dist/errorCodes.js.map +1 -1
  49. package/dist/errors.d.ts.map +1 -1
  50. package/dist/errors.js +16 -1
  51. package/dist/errors.js.map +1 -1
  52. package/dist/index.d.ts +1 -1
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js.map +1 -1
  55. package/dist/sessions/client.d.ts +29 -0
  56. package/dist/sessions/client.d.ts.map +1 -0
  57. package/dist/sessions/client.js +69 -0
  58. package/dist/sessions/client.js.map +1 -0
  59. package/dist/sessions/contract.d.ts +97 -0
  60. package/dist/sessions/contract.d.ts.map +1 -0
  61. package/dist/sessions/contract.js +5 -0
  62. package/dist/sessions/contract.js.map +1 -0
  63. package/dist/{auth/sessionMint.d.ts → sessions/create.d.ts} +4 -4
  64. package/dist/sessions/create.d.ts.map +1 -0
  65. package/dist/{auth/sessionMint.js → sessions/create.js} +11 -10
  66. package/dist/sessions/create.js.map +1 -0
  67. package/dist/sessions/handler.d.ts +23 -0
  68. package/dist/sessions/handler.d.ts.map +1 -0
  69. package/dist/sessions/handler.js +55 -0
  70. package/dist/sessions/handler.js.map +1 -0
  71. package/dist/sessions/index.d.ts +14 -0
  72. package/dist/sessions/index.d.ts.map +1 -0
  73. package/dist/sessions/index.js +14 -0
  74. package/dist/sessions/index.js.map +1 -0
  75. package/dist/{transport/connection/credentialLifecycle.d.ts → sessions/lifecycle.d.ts} +6 -6
  76. package/dist/sessions/lifecycle.d.ts.map +1 -0
  77. package/dist/{transport/connection/credentialLifecycle.js → sessions/lifecycle.js} +5 -5
  78. package/dist/sessions/lifecycle.js.map +1 -0
  79. package/dist/sessions/source.d.ts +24 -0
  80. package/dist/sessions/source.d.ts.map +1 -0
  81. package/dist/sessions/source.js +56 -0
  82. package/dist/sessions/source.js.map +1 -0
  83. package/dist/transport/connection/index.d.ts +0 -1
  84. package/dist/transport/connection/index.d.ts.map +1 -1
  85. package/dist/transport/connection/index.js +0 -1
  86. package/dist/transport/connection/index.js.map +1 -1
  87. package/dist/transport/connection/manager.d.ts +1 -1
  88. package/dist/transport/connection/manager.d.ts.map +1 -1
  89. package/dist/transport/http/client.d.ts +44 -16
  90. package/dist/transport/http/client.d.ts.map +1 -1
  91. package/dist/transport/http/client.js +125 -12
  92. package/dist/transport/http/client.js.map +1 -1
  93. package/dist/transport/http/options.d.ts +4 -1
  94. package/dist/transport/http/options.d.ts.map +1 -1
  95. package/dist/transport/http/transport.d.ts +25 -10
  96. package/dist/transport/http/transport.d.ts.map +1 -1
  97. package/dist/transport/http/transport.js +102 -56
  98. package/dist/transport/http/transport.js.map +1 -1
  99. package/dist/transport/websocket/commitFrames.d.ts +5 -5
  100. package/dist/transport/websocket/commitFrames.d.ts.map +1 -1
  101. package/dist/transport/websocket/commitFrames.js +1 -0
  102. package/dist/transport/websocket/commitFrames.js.map +1 -1
  103. package/dist/transport/websocket/contextSubscription.d.ts +6 -0
  104. package/dist/transport/websocket/contextSubscription.d.ts.map +1 -0
  105. package/dist/transport/websocket/contextSubscription.js +45 -0
  106. package/dist/transport/websocket/contextSubscription.js.map +1 -0
  107. package/dist/transport/websocket/frameHandlers.d.ts +0 -14
  108. package/dist/transport/websocket/frameHandlers.d.ts.map +1 -1
  109. package/dist/transport/websocket/frameHandlers.js +1 -71
  110. package/dist/transport/websocket/frameHandlers.js.map +1 -1
  111. package/dist/transport/websocket/session.d.ts +4 -0
  112. package/dist/transport/websocket/session.d.ts.map +1 -0
  113. package/dist/transport/websocket/session.js +425 -0
  114. package/dist/transport/websocket/session.js.map +1 -0
  115. package/dist/transport/websocket/sessionContract.d.ts +78 -0
  116. package/dist/transport/websocket/sessionContract.d.ts.map +1 -0
  117. package/dist/transport/websocket/sessionContract.js +2 -0
  118. package/dist/transport/websocket/sessionContract.js.map +1 -0
  119. package/dist/transport/websocket/transport.d.ts +27 -42
  120. package/dist/transport/websocket/transport.d.ts.map +1 -1
  121. package/dist/transport/websocket/transport.js +77 -102
  122. package/dist/transport/websocket/transport.js.map +1 -1
  123. package/dist/wire/inboundFrames.d.ts +0 -10
  124. package/dist/wire/inboundFrames.d.ts.map +1 -1
  125. package/dist/wire/inboundFrames.js +1 -6
  126. package/dist/wire/inboundFrames.js.map +1 -1
  127. package/package.json +7 -1
  128. package/src/auth/apiKey.ts +25 -44
  129. package/src/auth/browserCredentialSafety.ts +1 -1
  130. package/src/auth/capabilityLifecycle.ts +3 -2
  131. package/src/auth/credentialEndpoint.ts +1 -1
  132. package/src/auth/credentialResult.ts +2 -0
  133. package/src/claims/events.ts +1 -2
  134. package/src/client/ablo.ts +23 -13
  135. package/src/client/resources/httpResources.ts +2 -174
  136. package/src/client/resources/modelOperations.ts +0 -10
  137. package/src/client/surface.ts +9 -4
  138. package/src/commit/recordRuntime.ts +4 -2
  139. package/src/coordination/index.ts +2 -9
  140. package/src/coordination/schema.ts +7 -81
  141. package/src/errorCodes.ts +8 -13
  142. package/src/errors.ts +18 -1
  143. package/src/index.ts +1 -0
  144. package/src/sessions/client.ts +121 -0
  145. package/src/sessions/contract.ts +116 -0
  146. package/src/{auth/sessionMint.ts → sessions/create.ts} +13 -12
  147. package/src/sessions/handler.ts +96 -0
  148. package/src/sessions/index.ts +13 -0
  149. package/src/{transport/connection/credentialLifecycle.ts → sessions/lifecycle.ts} +5 -5
  150. package/src/sessions/source.ts +94 -0
  151. package/src/transport/connection/index.ts +0 -1
  152. package/src/transport/connection/manager.ts +1 -1
  153. package/src/transport/http/client.ts +183 -26
  154. package/src/transport/http/options.ts +4 -1
  155. package/src/transport/http/transport.ts +140 -78
  156. package/src/transport/websocket/commitFrames.ts +5 -5
  157. package/src/transport/websocket/contextSubscription.ts +55 -0
  158. package/src/transport/websocket/frameHandlers.ts +0 -102
  159. package/src/transport/websocket/session.ts +522 -0
  160. package/src/transport/websocket/sessionContract.ts +106 -0
  161. package/src/transport/websocket/transport.ts +85 -137
  162. package/src/wire/inboundFrames.ts +0 -6
  163. package/dist/auth/sessionMint.d.ts.map +0 -1
  164. package/dist/auth/sessionMint.js.map +0 -1
  165. package/dist/transport/connection/credentialLifecycle.d.ts.map +0 -1
  166. package/dist/transport/connection/credentialLifecycle.js.map +0 -1
@@ -28,6 +28,7 @@ import {
28
28
  readProcessEnv,
29
29
  resolveApiKey,
30
30
  resolveApiKeyValue,
31
+ resolveCredentialValue,
31
32
  resolveAuthToken,
32
33
  resolveBaseURL,
33
34
  resolveBootstrapBaseUrl,
@@ -86,15 +87,10 @@ import type {
86
87
  HttpTransportList,
87
88
  HttpLogsResource,
88
89
  ModelTarget,
89
- CreateSessionParams,
90
- AbloSession,
91
- SessionResource,
92
90
  } from '../../client/resources/httpResources.js';
93
- import { mintSession } from '../../auth/sessionMint.js';
94
- import {
95
- revokeCapability,
96
- rotateCapability,
97
- } from '../../auth/capabilityLifecycle.js';
91
+ import type { CredentialProviderResult } from '../../auth/credentialResult.js';
92
+ import { credentialToken } from '../../auth/credentialResult.js';
93
+ import { createSessionAccess, type SessionAccess } from '../../sessions/index.js';
98
94
  import { parseIdentityResolveResponse } from '../../auth/schemas.js';
99
95
  import type { EffectiveAuthority } from '../../auth/capability.js';
100
96
 
@@ -103,7 +99,6 @@ import {
103
99
  heldHeartbeatReply,
104
100
  parseSuccessfulCommitResponse,
105
101
  } from './helpers.js';
106
- import type { SchemaRecord } from '../../schema/schema.js';
107
102
  import type {
108
103
  ClaimLookupParams,
109
104
  ClaimOptions,
@@ -124,6 +119,12 @@ import {
124
119
  } from '../../client/resources/modelOperations.js';
125
120
  import type { Duration } from '../../utils/duration.js';
126
121
  import { claimDescription, partName } from '../../coordination/schema.js';
122
+ import type {
123
+ ClaimAcquired,
124
+ ClaimBeginPayload,
125
+ ClaimGranted,
126
+ ClaimQueued,
127
+ } from '../../coordination/schema.js';
127
128
  import type { BatchFence } from '../../claims/locator.js';
128
129
  import {
129
130
  subTarget,
@@ -169,12 +170,11 @@ import {
169
170
  } from '../../commit/httpRequest.js';
170
171
  import { resolveDurableWrites } from '../../commit/durableWrites.js';
171
172
  import { createHttpReadOnChange, type HttpReadOnChange } from './subscription.js';
173
+ import type { CommitFrameOperation } from '../websocket/commitFrames.js';
172
174
 
173
175
  /** @internal Private options for the schema-agnostic HTTP protocol transport. */
174
176
  export type HttpTransportOptions = Omit<HttpClientConfig, 'schema'> & {
175
177
  readonly bootstrapBaseUrl?: string | undefined;
176
- /** Schema-key to wire-typename mapping used only when minting agent sessions. */
177
- readonly modelTypenames?: Readonly<Record<string, string>> | undefined;
178
178
  /**
179
179
  * The observability provider forwarded from `Ablo({ observability })`. The HTTP
180
180
  * transport emits the same claim and conflict events as the WebSocket transport,
@@ -191,14 +191,29 @@ export type HttpTransportOptions = Omit<HttpClientConfig, 'schema'> & {
191
191
  * @default 30_000
192
192
  */
193
193
  readonly timeoutMs?: number;
194
+ /** @internal Routes writes through the selected duplex transport. */
195
+ readonly dispatchCommit?: ((input: {
196
+ readonly clientTxId: string;
197
+ readonly operations: readonly CommitFrameOperation[];
198
+ readonly reads?: readonly import('../../coordination/schema.js').ReadDependency[] | null;
199
+ }) => Promise<CommitReceiptWire>) | undefined;
200
+ /** @internal Routes held-claim acquisition through the shared socket. */
201
+ readonly dispatchClaim?: ((input: ClaimBeginPayload & {
202
+ readonly timeoutMs?: number;
203
+ readonly signal?: AbortSignal;
204
+ readonly onQueued?: (event: ClaimQueued) => Error | undefined;
205
+ }) => Promise<ClaimAcquired | ClaimGranted>) | undefined;
206
+ /** @internal Releases a socket-acquired claim on that same session. */
207
+ readonly releaseDispatchedClaim?: ((input: {
208
+ readonly claimId: string;
209
+ readonly entityType: string;
210
+ readonly entityId: string;
211
+ }) => void) | undefined;
194
212
  };
195
213
 
196
214
  /** @internal Default per-request deadline for the private HTTP transport. */
197
215
  export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
198
216
  const HTTP_CONFIRMATION_POLL_INTERVAL_MS = 250;
199
- // NOTE: end-user / agent session minting is `ablo.sessions.create(...)` (typed
200
- // against the schema, see Ablo.ts `CreateSessionParams`). There is no separate
201
- // `ephemeralKeys` resource — `sessions` is the one front door for both.
202
217
 
203
218
  /** @internal Private protocol surface wrapped by `AbloHttpClient`. */
204
219
  export interface HttpTransport {
@@ -230,12 +245,8 @@ export interface HttpTransport {
230
245
  * server with the credential this client already holds — no re-mint.
231
246
  */
232
247
  getAuthToken(): Promise<string | null>;
233
- /**
234
- * Mint a short-lived scoped session. Minting is a control-plane HTTP call (no
235
- * socket), so it lives on this stateless client too, not only the realtime one.
236
- * `{ user }` mints an `ek_`; `{ agent, can }` mints an `rk_`.
237
- */
238
- readonly sessions: SessionResource<SchemaRecord>;
248
+ /** @internal One normalized source shared by HTTP bootstrap and live transport. */
249
+ readonly access: SessionAccess;
239
250
  }
240
251
 
241
252
  type CommitResponse = CommitReceiptWire;
@@ -441,6 +452,12 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
441
452
  return headers;
442
453
  }
443
454
 
455
+ async function activeCredential(): Promise<CredentialProviderResult> {
456
+ return (await resolveCredentialValue(configuredApiKey)) ?? configuredAuthToken ?? null;
457
+ }
458
+
459
+ const access = createSessionAccess(options.session, activeCredential);
460
+
444
461
  function endpoint(path: string): string {
445
462
  const target = new URL(`${apiBaseUrl}${path.startsWith('/') ? path : `/${path}`}`);
446
463
  for (const [key, value] of Object.entries(options.defaultQuery ?? {})) {
@@ -981,11 +998,22 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
981
998
  path: string;
982
999
  body: unknown;
983
1000
  wait: CommitWait;
1001
+ operations: readonly CommitFrameOperation[];
1002
+ reads?: readonly import('../../coordination/schema.js').ReadDependency[] | null;
984
1003
  },
985
1004
  beforeConfirmation?: (response: CommitResponse) => Promise<void>
986
1005
  ): Promise<CommitResponse> {
987
1006
  return runInHttpCommitLane(async () => {
988
1007
  await prepare();
1008
+ if (options.dispatchCommit) {
1009
+ const response = await options.dispatchCommit({
1010
+ clientTxId: input.idempotencyKey,
1011
+ operations: input.operations,
1012
+ ...(input.reads !== undefined ? { reads: input.reads } : {}),
1013
+ });
1014
+ if (response.status === 'confirmed') await beforeConfirmation?.(response);
1015
+ return response;
1016
+ }
989
1017
  // Startup preparation covers replay. Re-draining here makes every later write wait
990
1018
  // behind an ambiguous predecessor from this same process.
991
1019
  const replayed = await replayHttpCommitOutbox();
@@ -1296,6 +1324,17 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1296
1324
  reads: commitOptions.reads,
1297
1325
  };
1298
1326
  const wait = commitOptions.wait ?? 'confirmed';
1327
+ const frameOperations: CommitFrameOperation[] = operations.map((operation) => ({
1328
+ type: operation.action.toUpperCase(),
1329
+ model: operation.model,
1330
+ id: operation.id ?? null,
1331
+ input: operation.data ?? null,
1332
+ where: operation.where ?? null,
1333
+ transactionId: operation.transactionId ?? null,
1334
+ claimId: operation.claimId ?? null,
1335
+ readAt: operation.readAt ?? null,
1336
+ fenceToken: operation.fenceToken ?? null,
1337
+ }));
1299
1338
  let body: CommitResponse;
1300
1339
  try {
1301
1340
  body = await dispatchHttpCommit({
@@ -1304,6 +1343,8 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1304
1343
  idempotencyKey: clientTxId,
1305
1344
  body: requestBody,
1306
1345
  wait,
1346
+ operations: frameOperations,
1347
+ reads: commitOptions.reads,
1307
1348
  });
1308
1349
  } catch (error) {
1309
1350
  // Coordination collision over HTTP — surface it to observability on the
@@ -1529,6 +1570,17 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1529
1570
  if (action === 'create') requestBody.id = id;
1530
1571
  if (data !== undefined) requestBody.data = data;
1531
1572
 
1573
+ const frameOperation: CommitFrameOperation = {
1574
+ type: action.toUpperCase(),
1575
+ model: modelName,
1576
+ id: action === 'create' ? (id || null) : id,
1577
+ input: data ?? null,
1578
+ transactionId: null,
1579
+ claimId: normalizeClaimId(options?.claimRef) ?? claimHandle?.id ?? null,
1580
+ readAt: readAt ?? null,
1581
+ fenceToken: options?.fenceToken ?? claimHandle?.fenceToken ?? null,
1582
+ };
1583
+
1532
1584
  let body: CommitResponse;
1533
1585
  try {
1534
1586
  body = await dispatchHttpCommit(
@@ -1541,6 +1593,8 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1541
1593
  // confirmation. Early queued receipts remain available through the
1542
1594
  // lower-level `commits.create` lifecycle surface.
1543
1595
  wait: 'confirmed',
1596
+ operations: [frameOperation],
1597
+ reads: options?.reads,
1544
1598
  },
1545
1599
  beforeConfirmation
1546
1600
  );
@@ -1579,6 +1633,59 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1579
1633
  params: ClaimParams<Fields>
1580
1634
  ): Promise<{ id: string; fenceToken?: number }> => {
1581
1635
  const contention = resolveClaimContentionOptions(params);
1636
+ if (options.dispatchClaim) {
1637
+ const claimId = createClientTxId();
1638
+ let waited = false;
1639
+ try {
1640
+ const grant = await options.dispatchClaim({
1641
+ claimId,
1642
+ entityType: name,
1643
+ entityId: params.id,
1644
+ ...subTarget(params, name),
1645
+ description: claimDescription(params),
1646
+ ...(params.ttl !== undefined ? { estimatedMs: toMs(params.ttl) } : {}),
1647
+ queue: contention.wait,
1648
+ ...(contention.timeoutMs !== undefined ? { timeoutMs: contention.timeoutMs } : {}),
1649
+ ...(contention.signal !== undefined ? { signal: contention.signal } : {}),
1650
+ onQueued: (event) => {
1651
+ waited = true;
1652
+ emitClaimStatus(contention.onStatus, {
1653
+ type: 'queued',
1654
+ claimId: event.claimId,
1655
+ position: event.position,
1656
+ ahead: event.position + 1,
1657
+ });
1658
+ if (
1659
+ contention.maxDepth !== undefined
1660
+ && event.position >= contention.maxDepth
1661
+ ) {
1662
+ return new AbloClaimedError(
1663
+ `Claim queue for ${name}/${params.id} is ${event.position} deep (max ${contention.maxDepth}).`,
1664
+ { code: 'queue_too_deep' },
1665
+ );
1666
+ }
1667
+ return undefined;
1668
+ },
1669
+ });
1670
+ emitClaimStatus(contention.onStatus, {
1671
+ type: 'granted',
1672
+ claimId: grant.claimId,
1673
+ waited,
1674
+ });
1675
+ return grant.fenceToken !== undefined
1676
+ ? { id: grant.claimId, fenceToken: grant.fenceToken }
1677
+ : { id: grant.claimId };
1678
+ } catch (error) {
1679
+ const normalized = error instanceof AbloError
1680
+ ? error
1681
+ : new AbloConnectionError(String(error));
1682
+ emitClaimStatus(
1683
+ contention.onStatus,
1684
+ claimAttemptFailure(contention.wait, normalized),
1685
+ );
1686
+ throw error;
1687
+ }
1688
+ }
1582
1689
  // The row is named by the URL, so `target` carries only the narrowing a
1583
1690
  // claim adds below it. Sending it is what makes a field-scoped claim
1584
1691
  // actually field-scoped: the server's conflict rule reads `path`,
@@ -1656,10 +1763,17 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1656
1763
  };
1657
1764
  const releaseClaim = (
1658
1765
  params: ClaimLookupParams<T> | ClaimParams<Fields> | Claim<T>,
1659
- ): Promise<void> =>
1660
- requestRaw(claimPath(isClaimHandle(params) ? params.target.id : params.id), {
1766
+ claimId?: string,
1767
+ ): Promise<void> => {
1768
+ const entityId = isClaimHandle(params) ? params.target.id : params.id;
1769
+ if (claimId && options.releaseDispatchedClaim) {
1770
+ options.releaseDispatchedClaim({ claimId, entityType: name, entityId });
1771
+ return Promise.resolve();
1772
+ }
1773
+ return requestRaw(claimPath(entityId), {
1661
1774
  method: 'DELETE',
1662
1775
  }).then(() => undefined);
1776
+ };
1663
1777
 
1664
1778
  // One beat on the held lease. A lapsed lease answers `claim_lost`
1665
1779
  // (409), which the wire error mapping surfaces as AbloClaimedError —
@@ -1763,7 +1877,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1763
1877
 
1764
1878
  const release = () => {
1765
1879
  stopHeartbeatLoop?.();
1766
- return releaseClaim(params);
1880
+ return releaseClaim(params, claimId);
1767
1881
  };
1768
1882
  // The handle handed back is a public claim, so its `meta` is the declared
1769
1883
  // shape — the same crossing the two decodes above make, spelled the same
@@ -1904,7 +2018,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1904
2018
  claim: undefined,
1905
2019
  });
1906
2020
  } finally {
1907
- await releaseClaim({ id }).catch(() => {});
2021
+ await releaseClaim({ id }, claimId).catch(() => {});
1908
2022
  }
1909
2023
  };
1910
2024
 
@@ -2100,64 +2214,12 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
2100
2214
  claims,
2101
2215
  logs,
2102
2216
  model,
2103
- sessions: {
2104
- async create(params: CreateSessionParams<SchemaRecord>): Promise<AbloSession> {
2105
- // Stateless mint: the configured key is the control-plane credential here
2106
- // (no startup `rk_` exchange runs on this client). It reuses the resolved
2107
- // base URL and fetch; the shared `mintSession` handles the two server routes.
2108
- const apiKey = await resolveApiKeyValue(configuredApiKey);
2109
- if (!apiKey) {
2110
- throw new AbloAuthenticationError(
2111
- 'sessions.create requires a secret (sk_) API key — call it from your backend, not the browser.',
2112
- { code: 'apikey_missing' }
2113
- );
2114
- }
2115
- // A transport built without a schema has no way to translate `can`'s
2116
- // schema keys into the type names the server gates on. Minting anyway
2117
- // would spell every override wrong and surface as
2118
- // `capability_scope_denied` on the agent's first write, so refuse here
2119
- // instead of guessing.
2120
- if (!options.modelTypenames) {
2121
- throw new AbloValidationError(
2122
- 'sessions.create needs the schema this client is bound to. Construct it ' +
2123
- "through Ablo({ schema, apiKey, transport: 'http' }) rather than the " +
2124
- 'bare transport.',
2125
- { code: 'invalid_options', param: 'schema' },
2126
- );
2127
- }
2128
- return mintSession(params, {
2129
- apiKey,
2130
- baseUrl: apiBaseUrl,
2131
- modelTypenames: options.modelTypenames,
2132
- ...(options.fetch ? { fetch: options.fetch } : {}),
2133
- });
2134
- },
2135
- async revoke({ id }) {
2136
- const apiKey = await resolveApiKeyValue(configuredApiKey);
2137
- return revokeCapability({
2138
- apiKey: apiKey ?? '',
2139
- baseUrl: apiBaseUrl,
2140
- id,
2141
- ...(options.fetch ? { fetch: options.fetch } : {}),
2142
- });
2143
- },
2144
- async rotate({ id, graceSeconds, ttlSeconds }) {
2145
- const apiKey = await resolveApiKeyValue(configuredApiKey);
2146
- return rotateCapability({
2147
- apiKey: apiKey ?? '',
2148
- baseUrl: apiBaseUrl,
2149
- id,
2150
- ...(graceSeconds !== undefined ? { graceSeconds } : {}),
2151
- ...(ttlSeconds !== undefined ? { ttlSeconds } : {}),
2152
- ...(options.fetch ? { fetch: options.fetch } : {}),
2153
- });
2154
- },
2155
- },
2156
2217
  async getAuthToken(): Promise<string | null> {
2157
2218
  // Mirror `authHeaders()`: a configured API key wins, else the
2158
2219
  // construction-time auth token. Resolve the (possibly async) key setter.
2159
- return (await resolveApiKeyValue(configuredApiKey)) ?? configuredAuthToken ?? null;
2220
+ return credentialToken(await activeCredential());
2160
2221
  },
2222
+ access,
2161
2223
  };
2162
2224
  }
2163
2225
 
@@ -17,7 +17,6 @@ import type {
17
17
  ClaimQueued,
18
18
  ClaimLost,
19
19
  ClaimRejection,
20
- ClaimExpired,
21
20
  } from '../../coordination/schema.js';
22
21
  import type { ClaimEvent, ClaimCounterparty } from '../../claims/events.js';
23
22
  import type { AssertExact } from '../../types/assertExact.js';
@@ -37,9 +36,10 @@ export type CommitAck = CanonicalCommitAck;
37
36
  export interface CommitFrameOperation {
38
37
  readonly type: string;
39
38
  readonly model: string;
40
- readonly id: string;
41
- readonly input?: Record<string, unknown>;
42
- readonly transactionId?: string;
39
+ readonly id: string | null;
40
+ readonly input?: Record<string, unknown> | null;
41
+ readonly where?: Record<string, unknown> | null;
42
+ readonly transactionId?: string | null;
43
43
  readonly claimId?: string | null;
44
44
  readonly readAt?: number | null;
45
45
  readonly fenceToken?: number | null;
@@ -65,6 +65,7 @@ export function buildCommitFrame(
65
65
  model: op.model,
66
66
  id: op.id,
67
67
  input: op.input,
68
+ where: op.where,
68
69
  transactionId: op.transactionId,
69
70
  claimId: op.claimId,
70
71
  readAt: op.readAt,
@@ -103,7 +104,6 @@ interface ClaimFrameByPhase {
103
104
  queued: ClaimQueued;
104
105
  lost: ClaimLost;
105
106
  rejected: ClaimRejection;
106
- expired: ClaimExpired;
107
107
  }
108
108
 
109
109
  // The phases this map covers and the phases a ClaimEvent can report are the
@@ -0,0 +1,55 @@
1
+ import type { ReadDependency } from '../../coordination/schema.js';
2
+ import { AbloStaleContextError } from '../../errors.js';
3
+ import type { AbloWebSocketSession } from './sessionContract.js';
4
+
5
+ /** Watch captured reads over the client's already-open WebSocket. */
6
+ export function subscribeWebSocketReadChanges(
7
+ session: AbloWebSocketSession,
8
+ reads: readonly ReadDependency[],
9
+ listener: (error: AbloStaleContextError) => void,
10
+ ): () => void {
11
+ if (reads.length === 0) return () => undefined;
12
+ const watchedRows = new Map(
13
+ reads
14
+ .filter((read): read is Extract<ReadDependency, { model: string }> => 'model' in read)
15
+ .map((read) => [`${read.model.toLowerCase()}:${read.id}`, read.readAt]),
16
+ );
17
+ const watchedGroups = new Map<string, number>(
18
+ reads
19
+ .filter((read): read is Extract<ReadDependency, { group: string }> => 'group' in read)
20
+ .map((read) => [read.group, read.readAt]),
21
+ );
22
+ let stopped = false;
23
+ let unsubscribe = (): void => undefined;
24
+ unsubscribe = session.subscribe('delta', (delta) => {
25
+ if (stopped) return;
26
+ const readAt = watchedRows.get(`${delta.modelName.toLowerCase()}:${delta.modelId}`)
27
+ ?? delta.syncGroups.reduce<number | undefined>((latest, group) => {
28
+ const groupReadAt = watchedGroups.get(group);
29
+ return groupReadAt === undefined
30
+ ? latest
31
+ : Math.max(latest ?? groupReadAt, groupReadAt);
32
+ }, undefined);
33
+ if (readAt === undefined || delta.id <= readAt) return;
34
+ stopped = true;
35
+ unsubscribe();
36
+ listener(new AbloStaleContextError(
37
+ `Captured context changed: ${delta.modelName}/${delta.modelId}.`,
38
+ {
39
+ code: 'stale_context',
40
+ details: {
41
+ conflicts: [{
42
+ model: delta.modelName,
43
+ id: delta.modelId,
44
+ readAt,
45
+ current: delta.id,
46
+ }],
47
+ },
48
+ },
49
+ ));
50
+ });
51
+ return () => {
52
+ stopped = true;
53
+ unsubscribe();
54
+ };
55
+ }
@@ -45,16 +45,6 @@ export interface PendingCommit {
45
45
  readonly returnReceipt?: boolean;
46
46
  }
47
47
 
48
- /**
49
- * In-flight `claim` request record, keyed by claimId. Resolved when the
50
- * matching `claim_ack` arrives, or rejected on timeout/disconnect.
51
- */
52
- export interface PendingClaim {
53
- resolve: (value: { syncGroups: string[]; ttlSeconds?: number }) => void;
54
- reject: (err: Error) => void;
55
- timeout: ReturnType<typeof setTimeout>;
56
- }
57
-
58
48
  /**
59
49
  * An in-flight `update_subscription` request awaiting its
60
50
  * `subscription_ack`. The wire carries no correlation id, so requests are
@@ -109,8 +99,6 @@ export interface WsSession {
109
99
  observability: SocketObservability;
110
100
  /** In-flight commit acks keyed by clientTxId. */
111
101
  pendingMutations: Map<string, PendingCommit>;
112
- /** In-flight claim acks keyed by claimId. */
113
- pendingClaims: Map<string, PendingClaim>;
114
102
  /** Removes and returns the oldest in-flight `update_subscription` request. */
115
103
  shiftPendingSubscription(): PendingSubscription | undefined;
116
104
  /** Connection options subset the handlers write back (acked sync groups). */
@@ -290,84 +278,6 @@ const handleMutationResult: WsFrameHandler = (session, message) => {
290
278
  }
291
279
  };
292
280
 
293
- /**
294
- * Handles the acknowledgement of a `claim` request. The frame has the shape
295
- * `{ type: 'claim_ack', payload: { claimId, success, syncGroups?,
296
- * ttlSeconds?, error? } }`.
297
- */
298
- const handleClaimAck: WsFrameHandler = (session, message) => {
299
- const p = (message.payload ?? {}) as Record<string, unknown>;
300
- const { claimId, success, syncGroups, ttlSeconds, error } = p;
301
- const pending =
302
- typeof claimId === 'string'
303
- ? session.pendingClaims.get(claimId)
304
- : undefined;
305
- if (!pending) return;
306
- clearTimeout(pending.timeout);
307
- // `pending` exists ⇒ claimId was a string key (the guard above).
308
- session.pendingClaims.delete(claimId as string);
309
- if (success) {
310
- pending.resolve({
311
- syncGroups: Array.isArray(syncGroups) ? syncGroups : [],
312
- ttlSeconds: typeof ttlSeconds === 'number' ? ttlSeconds : undefined,
313
- });
314
- } else {
315
- const err = error as
316
- | {
317
- code?: unknown;
318
- message?: unknown;
319
- request_id?: unknown;
320
- event_id?: unknown;
321
- }
322
- | undefined;
323
- const code =
324
- err?.code && typeof err.code === 'string'
325
- ? err.code
326
- : 'claim_rejected';
327
- const msg =
328
- err?.message && typeof err.message === 'string'
329
- ? err.message
330
- : 'claim rejected by server';
331
- const requestId =
332
- typeof err?.request_id === 'string' ? err.request_id : undefined;
333
- const eventId =
334
- typeof err?.event_id === 'string' ? err.event_id : undefined;
335
- const details = eventId === undefined ? undefined : { event_id: eventId };
336
- // Capability denials get the typed CapabilityError so
337
- // callers can read `.requiredCapability` and attenuate-
338
- // and-retry the claim with a narrower token.
339
- if (
340
- code === 'capability_scope_denied' ||
341
- code === 'capability_invalid'
342
- ) {
343
- const rc = (error as { requiredCapability?: unknown } | undefined)
344
- ?.requiredCapability;
345
- const requiredCapability =
346
- rc != null &&
347
- typeof rc === 'object' &&
348
- typeof (rc as { scope?: unknown }).scope === 'string'
349
- ? (rc as RequiredCapability)
350
- : undefined;
351
- pending.reject(
352
- new CapabilityError(code, msg, requiredCapability, {
353
- ...(requestId !== undefined ? { requestId } : {}),
354
- ...(details !== undefined ? { details } : {}),
355
- }),
356
- );
357
- } else {
358
- // Route through the shared factory so a failed claim_ack is a
359
- // typed AbloError (registry code → right subclass), symmetric
360
- // with the commit `mutation_result` path — never a bare Error.
361
- pending.reject(
362
- errorFromWire(msg, {
363
- code,
364
- requestId,
365
- details,
366
- }),
367
- );
368
- }
369
- }
370
- };
371
281
 
372
282
  /**
373
283
  * Handles the acknowledgement of an `update_subscription` request. The wire
@@ -448,7 +358,6 @@ export const wsFrameHandlers: Record<string, WsFrameHandler> = {
448
358
  bootstrap_response: (session, message) => { session.handleBootstrapResponse(message.payload); },
449
359
  presence_update: (session, message) => { session.handlePresenceUpdate(message); },
450
360
  mutation_result: handleMutationResult,
451
- claim_ack: handleClaimAck,
452
361
  subscription_ack: handleSubscriptionAck,
453
362
  delta: handleDeltaFrame,
454
363
  };
@@ -497,17 +406,6 @@ const validatedFrameHandlers: Record<
497
406
  session.handlePresenceUpdate({ payload });
498
407
  },
499
408
  ),
500
- claim_expired: validating(
501
- WS_INBOUND_FRAMES.claim_expired.payload,
502
- 'claim_expired',
503
- (session, payload) => {
504
- // Server-initiated expiry. The claim is already inactive by the time
505
- // this arrives, so a consumer either re-claims with a fresh credential
506
- // or accepts the drop.
507
- recordClaim(tracePorts(session), 'expired', payload);
508
- session.emit('claim_expired', payload);
509
- },
510
- ),
511
409
  claim_rejected: validating(
512
410
  WS_INBOUND_FRAMES.claim_rejected.payload,
513
411
  'claim_rejected',