@band-ai/band-sdk-core 2.8.0 → 2.10.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.
package/README.md CHANGED
@@ -68,6 +68,15 @@ snapshot names the same `id` twice. `RetryTracker`'s `maxTracked` must be
68
68
  at least `1`; `0` throws. `ClaimRegistry`'s `maxCompleted` must be at
69
69
  least `1` too — `0` also throws.
70
70
 
71
+ `ClaimRegistry` keeps one terminal intent per message until
72
+ `rememberCompleted` or matching `confirmSettlement`: `rememberAckPending` records a processed write and
73
+ `rememberFailedAckPending` a failed one carrying its `error`. The first
74
+ intent wins; a different one throws — leaving the entry unchanged, with
75
+ `.issues` and `.traceContext` attached — and an identical repeat is a
76
+ no-op. `pendingAck` returns `{status, error?, parked}` or `undefined`.
77
+ `parkAck` takes a `ParkedReason` wire name, `parkedReasons()` lists them,
78
+ and any other value throws a plain `Error`.
79
+
71
80
  `SubscriptionTracker` provides synchronous, transport-independent decisions for
72
81
  agent-topic joins and the two-topic room subscription transaction. Its opaque
73
82
  tickets are JavaScript `bigint` values and must be supplied when recording a
@@ -293,3 +302,30 @@ just build-wasm # Node package output
293
302
  ## License
294
303
 
295
304
  MIT — see [LICENSE](https://github.com/band-ai/band-sdk-core/blob/main/LICENSE).
305
+
306
+ ## Fenced delivery settlement
307
+
308
+ `guardedClaim` shares eligibility across discovery paths. Its optional facts
309
+ object accepts boolean `self_echo`, `durably_processed`, `durably_failed`, and
310
+ `allow_failed_retry` fields. Only `allow_failed_retry` defaults to true. Unknown
311
+ fields and non-boolean values throw before ownership changes.
312
+
313
+ Pending originals remain excluded when parked. Record the terminal disposition
314
+ before transport and confirm only after acceptance and a current-execution check.
315
+ Retry confirmation restores charged attempts with the stricter retry ceiling;
316
+ it does not falsely complete the original. Tokens are opaque local strings.
317
+
318
+ ```ts
319
+ import { ClaimRegistry, RetryTracker } from "@band-ai/band-sdk-core";
320
+
321
+ const claims = new ClaimRegistry();
322
+ const previous = new RetryTracker(3);
323
+ if (claims.guardedClaim("room", "message", previous) !== "runnable") throw new Error("not admitted");
324
+ previous.recordAttempt("message");
325
+ const pending = claims.recordFailedSettlement("room", "message", "failure", previous, true);
326
+ claims.release("room", "message");
327
+ const current = new RetryTracker(3);
328
+ if (!claims.confirmSettlement("room", "message", pending.token, current)) throw new Error("not confirmed");
329
+ if (claims.isCompleted("room", "message")) throw new Error("retry falsely completed");
330
+ if (current.recordAttempt("message")[0] !== 2) throw new Error("charge lost");
331
+ ```
package/band_sdk_core.js CHANGED
@@ -118,8 +118,8 @@ exports.AgentFailure = AgentFailure;
118
118
 
119
119
  /**
120
120
  * Ownership ledger for the inbound message-delivery lifecycle: in-flight,
121
- * ack-pending, and completed states, each scoped by `(roomId,
122
- * messageId)`.
121
+ * ack-pending (a stored processed/failed intent, optionally parked), and
122
+ * completed states, each scoped by `(roomId, messageId)`.
123
123
  */
124
124
  class ClaimRegistry {
125
125
  __destroy_into_raw() {
@@ -145,6 +145,47 @@ class ClaimRegistry {
145
145
  wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
146
146
  return v1;
147
147
  }
148
+ /**
149
+ * @param {any} room_id
150
+ * @param {any} message_id
151
+ * @param {any} token
152
+ * @param {RetryTracker} retry_tracker
153
+ * @returns {boolean}
154
+ */
155
+ confirmSettlement(room_id, message_id, token, retry_tracker) {
156
+ _assertClass(retry_tracker, RetryTracker);
157
+ const ret = wasm.claimregistry_confirmSettlement(this.__wbg_ptr, room_id, message_id, token, retry_tracker.__wbg_ptr);
158
+ if (ret[2]) {
159
+ throw takeFromExternrefTable0(ret[1]);
160
+ }
161
+ return ret[0] !== 0;
162
+ }
163
+ /**
164
+ * @param {any} room_id
165
+ * @param {any} message_id
166
+ * @param {RetryTracker} retry_tracker
167
+ * @param {any} facts
168
+ * @returns {string}
169
+ */
170
+ deliveryEligibility(room_id, message_id, retry_tracker, facts) {
171
+ let deferred2_0;
172
+ let deferred2_1;
173
+ try {
174
+ _assertClass(retry_tracker, RetryTracker);
175
+ const ret = wasm.claimregistry_deliveryEligibility(this.__wbg_ptr, room_id, message_id, retry_tracker.__wbg_ptr, facts);
176
+ var ptr1 = ret[0];
177
+ var len1 = ret[1];
178
+ if (ret[3]) {
179
+ ptr1 = 0; len1 = 0;
180
+ throw takeFromExternrefTable0(ret[2]);
181
+ }
182
+ deferred2_0 = ptr1;
183
+ deferred2_1 = len1;
184
+ return getStringFromWasm0(ptr1, len1);
185
+ } finally {
186
+ wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
187
+ }
188
+ }
148
189
  /**
149
190
  * @param {any} room_id
150
191
  */
@@ -154,6 +195,43 @@ class ClaimRegistry {
154
195
  throw takeFromExternrefTable0(ret[0]);
155
196
  }
156
197
  }
198
+ /**
199
+ * @param {any} room_id
200
+ * @param {any} message_id
201
+ * @param {RetryTracker} retry_tracker
202
+ * @param {any} facts
203
+ * @returns {string}
204
+ */
205
+ guardedClaim(room_id, message_id, retry_tracker, facts) {
206
+ let deferred2_0;
207
+ let deferred2_1;
208
+ try {
209
+ _assertClass(retry_tracker, RetryTracker);
210
+ const ret = wasm.claimregistry_guardedClaim(this.__wbg_ptr, room_id, message_id, retry_tracker.__wbg_ptr, facts);
211
+ var ptr1 = ret[0];
212
+ var len1 = ret[1];
213
+ if (ret[3]) {
214
+ ptr1 = 0; len1 = 0;
215
+ throw takeFromExternrefTable0(ret[2]);
216
+ }
217
+ deferred2_0 = ptr1;
218
+ deferred2_1 = len1;
219
+ return getStringFromWasm0(ptr1, len1);
220
+ } finally {
221
+ wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
222
+ }
223
+ }
224
+ /**
225
+ * @param {any} room_id
226
+ * @returns {boolean}
227
+ */
228
+ hasRetryableSettlement(room_id) {
229
+ const ret = wasm.claimregistry_hasRetryableSettlement(this.__wbg_ptr, room_id);
230
+ if (ret[2]) {
231
+ throw takeFromExternrefTable0(ret[1]);
232
+ }
233
+ return ret[0] !== 0;
234
+ }
157
235
  /**
158
236
  * @param {any} room_id
159
237
  * @returns {string[]}
@@ -210,6 +288,35 @@ class ClaimRegistry {
210
288
  ClaimRegistryFinalization.register(this, this.__wbg_ptr, this);
211
289
  return this;
212
290
  }
291
+ /**
292
+ * Mark a pending entry parked for `reason` (`"not_found"` or
293
+ * `"rejected"`); `false` when nothing is pending for the message.
294
+ * @param {any} room_id
295
+ * @param {any} message_id
296
+ * @param {any} reason
297
+ * @returns {boolean}
298
+ */
299
+ parkAck(room_id, message_id, reason) {
300
+ const ret = wasm.claimregistry_parkAck(this.__wbg_ptr, room_id, message_id, reason);
301
+ if (ret[2]) {
302
+ throw takeFromExternrefTable0(ret[1]);
303
+ }
304
+ return ret[0] !== 0;
305
+ }
306
+ /**
307
+ * @param {any} room_id
308
+ * @param {any} message_id
309
+ * @param {any} token
310
+ * @param {any} reason
311
+ * @returns {boolean}
312
+ */
313
+ parkSettlement(room_id, message_id, token, reason) {
314
+ const ret = wasm.claimregistry_parkSettlement(this.__wbg_ptr, room_id, message_id, token, reason);
315
+ if (ret[2]) {
316
+ throw takeFromExternrefTable0(ret[1]);
317
+ }
318
+ return ret[0] !== 0;
319
+ }
213
320
  /**
214
321
  * @param {any} room_id
215
322
  * @returns {string[]}
@@ -223,6 +330,33 @@ class ClaimRegistry {
223
330
  wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
224
331
  return v1;
225
332
  }
333
+ /**
334
+ * The pending entry as `{status: "processed" | "failed", error?:
335
+ * string, parked: "not_found" | "rejected" | null}`, or `undefined`.
336
+ * @param {any} room_id
337
+ * @param {any} message_id
338
+ * @returns {any}
339
+ */
340
+ pendingAck(room_id, message_id) {
341
+ const ret = wasm.claimregistry_pendingAck(this.__wbg_ptr, room_id, message_id);
342
+ if (ret[2]) {
343
+ throw takeFromExternrefTable0(ret[1]);
344
+ }
345
+ return takeFromExternrefTable0(ret[0]);
346
+ }
347
+ /**
348
+ * @param {any} room_id
349
+ * @param {any} message_id
350
+ * @param {any} trace_context
351
+ * @returns {any}
352
+ */
353
+ pendingSettlement(room_id, message_id, trace_context) {
354
+ const ret = wasm.claimregistry_pendingSettlement(this.__wbg_ptr, room_id, message_id, trace_context);
355
+ if (ret[2]) {
356
+ throw takeFromExternrefTable0(ret[1]);
357
+ }
358
+ return takeFromExternrefTable0(ret[0]);
359
+ }
226
360
  /**
227
361
  * @param {any} room_id
228
362
  * @param {any} message_id
@@ -235,6 +369,36 @@ class ClaimRegistry {
235
369
  }
236
370
  return ret[0] >>> 0;
237
371
  }
372
+ /**
373
+ * @param {any} room_id
374
+ * @param {any} message_id
375
+ * @param {any} error
376
+ * @param {RetryTracker} retry_tracker
377
+ * @param {any} allow_retry
378
+ * @param {any} trace_context
379
+ * @returns {any}
380
+ */
381
+ recordFailedSettlement(room_id, message_id, error, retry_tracker, allow_retry, trace_context) {
382
+ _assertClass(retry_tracker, RetryTracker);
383
+ const ret = wasm.claimregistry_recordFailedSettlement(this.__wbg_ptr, room_id, message_id, error, retry_tracker.__wbg_ptr, allow_retry, trace_context);
384
+ if (ret[2]) {
385
+ throw takeFromExternrefTable0(ret[1]);
386
+ }
387
+ return takeFromExternrefTable0(ret[0]);
388
+ }
389
+ /**
390
+ * @param {any} room_id
391
+ * @param {any} message_id
392
+ * @param {any} trace_context
393
+ * @returns {any}
394
+ */
395
+ recordProcessedSettlement(room_id, message_id, trace_context) {
396
+ const ret = wasm.claimregistry_recordProcessedSettlement(this.__wbg_ptr, room_id, message_id, trace_context);
397
+ if (ret[2]) {
398
+ throw takeFromExternrefTable0(ret[1]);
399
+ }
400
+ return takeFromExternrefTable0(ret[0]);
401
+ }
238
402
  /**
239
403
  * @param {any} room_id
240
404
  * @param {any} message_id
@@ -265,6 +429,21 @@ class ClaimRegistry {
265
429
  throw takeFromExternrefTable0(ret[0]);
266
430
  }
267
431
  }
432
+ /**
433
+ * Record a failed turn awaiting a durable failed write carrying
434
+ * `error`. Throws if a different intent is already pending, with
435
+ * `.issues` and `.traceContext`.
436
+ * @param {any} room_id
437
+ * @param {any} message_id
438
+ * @param {any} error
439
+ * @param {any} traceContext
440
+ */
441
+ rememberFailedAckPending(room_id, message_id, error, traceContext) {
442
+ const ret = wasm.claimregistry_rememberFailedAckPending(this.__wbg_ptr, room_id, message_id, error, traceContext);
443
+ if (ret[1]) {
444
+ throw takeFromExternrefTable0(ret[0]);
445
+ }
446
+ }
268
447
  /**
269
448
  * @param {any} room_id
270
449
  * @param {any} message_id
@@ -277,6 +456,17 @@ class ClaimRegistry {
277
456
  }
278
457
  return ret[0] !== 0;
279
458
  }
459
+ /**
460
+ * @param {any} room_id
461
+ * @returns {number}
462
+ */
463
+ unparkAcks(room_id) {
464
+ const ret = wasm.claimregistry_unparkAcks(this.__wbg_ptr, room_id);
465
+ if (ret[2]) {
466
+ throw takeFromExternrefTable0(ret[1]);
467
+ }
468
+ return ret[0] >>> 0;
469
+ }
280
470
  }
281
471
  if (Symbol.dispose) ClaimRegistry.prototype[Symbol.dispose] = ClaimRegistry.prototype.free;
282
472
  exports.ClaimRegistry = ClaimRegistry;
@@ -1791,6 +1981,18 @@ function noReplyTool() {
1791
1981
  }
1792
1982
  exports.noReplyTool = noReplyTool;
1793
1983
 
1984
+ /**
1985
+ * r" Every `ClaimRegistry.parkAck` reason, in declaration order.
1986
+ * @returns {string[]}
1987
+ */
1988
+ function parkedReasons() {
1989
+ const ret = wasm.parkedReasons();
1990
+ var v1 = getArrayJsValueFromWasm0(ret[0], ret[1]);
1991
+ wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
1992
+ return v1;
1993
+ }
1994
+ exports.parkedReasons = parkedReasons;
1995
+
1794
1996
  /**
1795
1997
  * This platform's Phoenix Channels topic for a room's `room_participants`
1796
1998
  * channel.
@@ -1884,6 +2086,14 @@ exports.validateMemoryTypeForSystem = validateMemoryTypeForSystem;
1884
2086
  function __wbg_get_imports() {
1885
2087
  const import0 = {
1886
2088
  __proto__: null,
2089
+ __wbg_0_c2e12945caff5360: function(arg0) {
2090
+ const ret = arg0["0"];
2091
+ return ret;
2092
+ },
2093
+ __wbg_1_191c69f1437bcd10: function(arg0) {
2094
+ const ret = arg0["1"];
2095
+ return ret;
2096
+ },
1887
2097
  __wbg_Error_30c8987f7c2ed4e2: function(arg0, arg1) {
1888
2098
  const ret = Error(getStringFromWasm0(arg0, arg1));
1889
2099
  return ret;
@@ -1975,6 +2185,10 @@ function __wbg_get_imports() {
1975
2185
  const ret = arg0.done;
1976
2186
  return ret;
1977
2187
  },
2188
+ __wbg_entries_b6763c9f1557e886: function() { return handleError(function (arg0) {
2189
+ const ret = Object.entries(arg0);
2190
+ return ret;
2191
+ }, arguments); },
1978
2192
  __wbg_entries_fb6397112b1de25f: function(arg0) {
1979
2193
  const ret = Object.entries(arg0);
1980
2194
  return ret;
@@ -2043,6 +2257,10 @@ function __wbg_get_imports() {
2043
2257
  const ret = result;
2044
2258
  return ret;
2045
2259
  },
2260
+ __wbg_isArray_cff2824818301a8c: function() { return handleError(function (arg0) {
2261
+ const ret = Array.isArray(arg0);
2262
+ return ret;
2263
+ }, arguments); },
2046
2264
  __wbg_isArray_e15a2ff68ffdbef2: function(arg0) {
2047
2265
  const ret = Array.isArray(arg0);
2048
2266
  return ret;
Binary file
package/index.d.ts CHANGED
@@ -128,9 +128,43 @@ export interface ParticipantFields {
128
128
  description?: string | null;
129
129
  }
130
130
 
131
+ /**
132
+ * Why the platform refused a terminal write that stays pending:
133
+ * `not_found` when the message or room is not visible, `rejected` when it
134
+ * proved no processing attempt exists to settle.
135
+ */
136
+ export type ParkedReason = "not_found" | "rejected";
137
+
138
+ /** Every {@link ParkedReason} wire name, in declaration order. */
139
+ export function parkedReasons(): ParkedReason[];
140
+
141
+ /** A pending entry: its fixed terminal intent plus whether it is parked. */
142
+ export type PendingAck =
143
+ | { status: "processed"; parked: ParkedReason | null }
144
+ | { status: "failed"; error: string; parked: ParkedReason | null };
145
+
146
+ export type DeliveryEligibility = "self" | "completed" | "pending" | "inflight" | "exhausted" | "failed" | "runnable";
147
+
148
+ export interface DeliveryFacts {
149
+ self_echo?: boolean;
150
+ durably_processed?: boolean;
151
+ durably_failed?: boolean;
152
+ allow_failed_retry?: boolean;
153
+ }
154
+
155
+ export type SettlementContinuation =
156
+ | { kind: "finished" }
157
+ | { kind: "retry"; attempts: number; max_retries: number };
158
+
159
+ export type PendingSettlement = PendingAck & {
160
+ token: string;
161
+ continuation: SettlementContinuation;
162
+ };
163
+
131
164
  /**
132
165
  * Ownership ledger for the inbound message-delivery lifecycle: in-flight,
133
- * ack-pending, and completed states, each scoped by `(roomId, messageId)`.
166
+ * ack-pending (a stored processed/failed intent, optionally parked), and
167
+ * completed states, each scoped by `(roomId, messageId)`.
134
168
  *
135
169
  * Throws if `maxCompleted` is not a non-negative integer in range, or is
136
170
  * `0` -- a zero capacity would evict every completed entry the instant it
@@ -149,8 +183,30 @@ export class ClaimRegistry {
149
183
  rememberCompleted(roomId: string, messageId: string): void;
150
184
  isAckPending(roomId: string, messageId: string): boolean;
151
185
  rememberAckPending(roomId: string, messageId: string): void;
186
+ /**
187
+ * Throws if a different terminal intent is already pending, with `.issues`
188
+ * and `.traceContext`.
189
+ */
190
+ rememberFailedAckPending(
191
+ roomId: string,
192
+ messageId: string,
193
+ error: string,
194
+ traceContext?: string | null,
195
+ ): void;
196
+ pendingAck(roomId: string, messageId: string): PendingAck | undefined;
197
+ /** `false` when nothing is pending for the message; throws on an unknown reason. */
198
+ parkAck(roomId: string, messageId: string, reason: ParkedReason): boolean;
199
+ unparkAcks(roomId: string): number;
152
200
  pendingAckIds(roomId: string): string[];
153
201
  recordAckRetry(roomId: string, messageId: string): number;
202
+ deliveryEligibility(roomId: string, messageId: string, retryTracker: RetryTracker, facts?: DeliveryFacts): DeliveryEligibility;
203
+ guardedClaim(roomId: string, messageId: string, retryTracker: RetryTracker, facts?: DeliveryFacts): DeliveryEligibility;
204
+ hasRetryableSettlement(roomId: string): boolean;
205
+ pendingSettlement(roomId: string, messageId: string, traceContext?: string | null): PendingSettlement | undefined;
206
+ recordProcessedSettlement(roomId: string, messageId: string, traceContext?: string | null): PendingSettlement;
207
+ recordFailedSettlement(roomId: string, messageId: string, error: string, retryTracker: RetryTracker, allowRetry: boolean, traceContext?: string | null): PendingSettlement;
208
+ confirmSettlement(roomId: string, messageId: string, token: string, retryTracker: RetryTracker): boolean;
209
+ parkSettlement(roomId: string, messageId: string, token: string, reason: ParkedReason): boolean;
154
210
  }
155
211
 
156
212
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@band-ai/band-sdk-core",
3
- "version": "2.8.0",
3
+ "version": "2.10.0",
4
4
  "description": "Shared Band event-payload validation",
5
5
  "license": "MIT",
6
6
  "repository": {