@aexhq/sdk 0.46.3-canary → 0.46.5-canary

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.
@@ -45,8 +45,19 @@ export function hasRunTerminalType(event) {
45
45
  * the coordinator's pair (platform `packages/shared/src/event-stream-client.ts`).
46
46
  */
47
47
  const COORDINATOR_PING = "aex:ping";
48
- /** Post-open replay request; $connect cannot safely PostToConnection before the handshake completes. */
49
- const COORDINATOR_REPLAY = JSON.stringify({ action: "replay" });
48
+ /**
49
+ * Pull request for everything from `nextSequence` onward.
50
+ *
51
+ * Sent on open ($connect cannot safely PostToConnection before the handshake
52
+ * completes) and again whenever a frame declares a predecessor this client has
53
+ * not reached. Catch-up is PULL-based because the connection row holds only the
54
+ * server's BELIEF about what reached the socket: a frame that was posted but
55
+ * never arrived leaves the client behind that belief with no way for the server
56
+ * to notice. Naming the cursor makes the client authoritative about what it
57
+ * actually holds. An older coordinator ignores the extra field and falls back to
58
+ * its own high-water, so sending it is safe against any deployed server.
59
+ */
60
+ const replayFrom = (nextSequence) => JSON.stringify({ action: "replay", from: nextSequence });
50
61
  /** Default half-open watchdog window — 3× the ping cadence, so 2 pongs can be lost. */
51
62
  const DEFAULT_IDLE_TIMEOUT_MS = 45_000;
52
63
  /** Default client keep-alive ping cadence. */
@@ -98,6 +109,7 @@ export async function* streamCoordinatorEvents(opts) {
98
109
  const ws = makeWs(url.toString());
99
110
  const pending = [];
100
111
  const seenSequences = new Set();
112
+ const catchUp = createCatchUp(ws, cursor);
101
113
  let closed = false;
102
114
  let disconnectReason = "";
103
115
  let resolveNext = null;
@@ -199,15 +211,7 @@ export async function* streamCoordinatorEvents(opts) {
199
211
  };
200
212
  ws.addEventListener("open", () => {
201
213
  armIdle();
202
- if (typeof ws.send === "function") {
203
- try {
204
- ws.send(COORDINATOR_REPLAY);
205
- }
206
- catch {
207
- // socket not open / send unsupported — the DDB-stream replay kicker and
208
- // quiet-reconnect path still cover replay.
209
- }
210
- }
214
+ catchUp.request();
211
215
  if (pingIntervalMs > 0 && typeof ws.send === "function") {
212
216
  pingTimer = timerPort.setInterval(() => {
213
217
  try {
@@ -232,6 +236,11 @@ export async function* streamCoordinatorEvents(opts) {
232
236
  if (isReplayableEvent(evt)) {
233
237
  if (evt.sequence <= cursor || seenSequences.has(evt.sequence))
234
238
  return;
239
+ // A frame the client cannot chain is refused, leaving both the cursor
240
+ // and `seenSequences` untouched so the pull's copy of it is not later
241
+ // suppressed as a duplicate.
242
+ if (!catchUp.chains(evt))
243
+ return;
235
244
  seenSequences.add(evt.sequence);
236
245
  }
237
246
  else if (!rememberLiveId(evt.id)) {
@@ -325,6 +334,64 @@ export async function* streamCoordinatorEvents(opts) {
325
334
  await sleep(reconnectDelayMs, timerPort, opts.signal);
326
335
  }
327
336
  }
337
+ /**
338
+ * The exclusive lower bound the coordinator declared for a durable frame: the
339
+ * sequence of the public event immediately preceding it in journal projection
340
+ * order (`-1` when the frame opens the session).
341
+ *
342
+ * Transport-only — it describes this frame's position in the stream, not the
343
+ * event, so it is never part of the durable {@link AexEvent} the archive returns.
344
+ * Absent means the producer made no claim (an older coordinator, or a path that
345
+ * cannot prove what came before it), and an unproven link is not a gap: the
346
+ * frame is accepted exactly as it was before this check existed.
347
+ */
348
+ function declaredPredecessor(frame) {
349
+ const value = frame.prevSequence;
350
+ return typeof value === "number" && Number.isSafeInteger(value) ? value : undefined;
351
+ }
352
+ /**
353
+ * Per-connection catch-up bookkeeping.
354
+ *
355
+ * The high-water is RECEIPT-side — the highest durable sequence admitted on this
356
+ * connection, including frames still queued for the consumer. The gap check has
357
+ * to run against that and not against the yield-side cursor, which lags while
358
+ * the consumer drains: a contiguous burst landing in one turn would otherwise
359
+ * report every frame after the first as a hole.
360
+ *
361
+ * One pull is outstanding per high-water position, so a run of unchainable
362
+ * frames costs one request rather than one per frame; it re-arms by
363
+ * construction, since the next gap at a moved position names a different one.
364
+ */
365
+ function createCatchUp(ws, cursor) {
366
+ let accepted = cursor;
367
+ let pulledFrom = null;
368
+ const request = () => {
369
+ const from = accepted + 1;
370
+ if (pulledFrom === from || typeof ws.send !== "function")
371
+ return;
372
+ pulledFrom = from;
373
+ try {
374
+ ws.send(replayFrom(from));
375
+ }
376
+ catch {
377
+ // socket not open / send unsupported — the idle + quiet watchdogs still
378
+ // reconnect, and a reconnect resumes from the cursor anyway.
379
+ }
380
+ };
381
+ return {
382
+ request,
383
+ chains: (frame) => {
384
+ const predecessor = declaredPredecessor(frame);
385
+ if (predecessor !== undefined && predecessor > accepted) {
386
+ request();
387
+ return false;
388
+ }
389
+ if (frame.sequence > accepted)
390
+ accepted = frame.sequence;
391
+ return true;
392
+ }
393
+ };
394
+ }
328
395
  function isCoordinatorStreamEvent(value) {
329
396
  if (!value || typeof value !== "object" || Array.isArray(value))
330
397
  return false;
package/dist/cli.mjs CHANGED
@@ -3501,7 +3501,7 @@ function hasRunTerminalType(event) {
3501
3501
  return event.type === "RUN_FINISHED" || event.type === "RUN_ERROR";
3502
3502
  }
3503
3503
  var COORDINATOR_PING = "aex:ping";
3504
- var COORDINATOR_REPLAY = JSON.stringify({ action: "replay" });
3504
+ var replayFrom = (nextSequence) => JSON.stringify({ action: "replay", from: nextSequence });
3505
3505
  var DEFAULT_IDLE_TIMEOUT_MS = 45e3;
3506
3506
  var DEFAULT_PING_INTERVAL_MS = 15e3;
3507
3507
  var DEFAULT_EVENT_QUIET_RECHECK_MS = 9e4;
@@ -3546,6 +3546,7 @@ async function* streamCoordinatorEvents(opts) {
3546
3546
  const ws = makeWs(url.toString());
3547
3547
  const pending = [];
3548
3548
  const seenSequences = /* @__PURE__ */ new Set();
3549
+ const catchUp = createCatchUp(ws, cursor);
3549
3550
  let closed = false;
3550
3551
  let disconnectReason = "";
3551
3552
  let resolveNext = null;
@@ -3639,12 +3640,7 @@ async function* streamCoordinatorEvents(opts) {
3639
3640
  };
3640
3641
  ws.addEventListener("open", () => {
3641
3642
  armIdle();
3642
- if (typeof ws.send === "function") {
3643
- try {
3644
- ws.send(COORDINATOR_REPLAY);
3645
- } catch {
3646
- }
3647
- }
3643
+ catchUp.request();
3648
3644
  if (pingIntervalMs > 0 && typeof ws.send === "function") {
3649
3645
  pingTimer = timerPort.setInterval(() => {
3650
3646
  try {
@@ -3667,6 +3663,8 @@ async function* streamCoordinatorEvents(opts) {
3667
3663
  if (isReplayableEvent(evt)) {
3668
3664
  if (evt.sequence <= cursor || seenSequences.has(evt.sequence))
3669
3665
  return;
3666
+ if (!catchUp.chains(evt))
3667
+ return;
3670
3668
  seenSequences.add(evt.sequence);
3671
3669
  } else if (!rememberLiveId(evt.id)) {
3672
3670
  return;
@@ -3742,6 +3740,37 @@ async function* streamCoordinatorEvents(opts) {
3742
3740
  await sleep(reconnectDelayMs, timerPort, opts.signal);
3743
3741
  }
3744
3742
  }
3743
+ function declaredPredecessor(frame) {
3744
+ const value = frame.prevSequence;
3745
+ return typeof value === "number" && Number.isSafeInteger(value) ? value : void 0;
3746
+ }
3747
+ function createCatchUp(ws, cursor) {
3748
+ let accepted = cursor;
3749
+ let pulledFrom = null;
3750
+ const request = () => {
3751
+ const from = accepted + 1;
3752
+ if (pulledFrom === from || typeof ws.send !== "function")
3753
+ return;
3754
+ pulledFrom = from;
3755
+ try {
3756
+ ws.send(replayFrom(from));
3757
+ } catch {
3758
+ }
3759
+ };
3760
+ return {
3761
+ request,
3762
+ chains: (frame) => {
3763
+ const predecessor = declaredPredecessor(frame);
3764
+ if (predecessor !== void 0 && predecessor > accepted) {
3765
+ request();
3766
+ return false;
3767
+ }
3768
+ if (frame.sequence > accepted)
3769
+ accepted = frame.sequence;
3770
+ return true;
3771
+ }
3772
+ };
3773
+ }
3745
3774
  function isCoordinatorStreamEvent(value) {
3746
3775
  if (!value || typeof value !== "object" || Array.isArray(value))
3747
3776
  return false;
@@ -1 +1 @@
1
- 04c687d3f3d7cbaf6b923b5d969edc4c98de596229d944442e9b6a703c83fd69 cli.mjs
1
+ a57352fbb791f3941e71e1c75b56cbe77a1c7616be763c021fe5b37e316a9928 cli.mjs
package/dist/version.d.ts CHANGED
@@ -6,4 +6,4 @@
6
6
  *
7
7
  * Used by the (future) User-Agent header on outbound SDK requests.
8
8
  */
9
- export declare const SDK_VERSION = "0.46.3-canary";
9
+ export declare const SDK_VERSION = "0.46.5-canary";
package/dist/version.js CHANGED
@@ -6,5 +6,5 @@
6
6
  *
7
7
  * Used by the (future) User-Agent header on outbound SDK requests.
8
8
  */
9
- export const SDK_VERSION = "0.46.3-canary";
9
+ export const SDK_VERSION = "0.46.5-canary";
10
10
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexhq/sdk",
3
- "version": "0.46.3-canary",
3
+ "version": "0.46.5-canary",
4
4
  "description": "TypeScript SDK for autonomous agent sessions with managed Vercel AI Gateway model access.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -55,6 +55,6 @@
55
55
  "zod": "4.4.3"
56
56
  },
57
57
  "aexRelease": {
58
- "sourceSha": "37ad2edae1eac28f413615e25d50d330c45a4c2f"
58
+ "sourceSha": "b2bf9ebe5c7e86a89e37f48c9c93ef359252202e"
59
59
  }
60
60
  }