@astralform/js 4.9.0 → 4.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1338,6 +1338,7 @@ function translateWireEvent(wire) {
1338
1338
 
1339
1339
  // src/session.ts
1340
1340
  var SSE_MAX_RECONNECTS = 6;
1341
+ var SSE_STALL_TIMEOUT_MS = 45e3;
1341
1342
  var TOOL_RESULT_MAX_RETRIES = 3;
1342
1343
  var CONVERSATION_PAGE_SIZE = 50;
1343
1344
  function sseReconnectDelayMs(attempt) {
@@ -1589,14 +1590,23 @@ var ChatSession = class {
1589
1590
  async consumeEventStream(jobId, conversationId, messageId, executeClientTools) {
1590
1591
  const signal = this.abortController?.signal;
1591
1592
  for (let attempt = 0; ; attempt++) {
1592
- const stream = this.client.streamJobEvents(jobId, this.lastSeq, signal);
1593
+ if (signal?.aborted) return;
1594
+ const attemptController = new AbortController();
1595
+ const linkAbort = () => attemptController.abort();
1596
+ signal?.addEventListener("abort", linkAbort);
1593
1597
  let sawTerminal;
1594
1598
  try {
1599
+ const stream = this.client.streamJobEvents(
1600
+ jobId,
1601
+ this.lastSeq,
1602
+ attemptController.signal
1603
+ );
1595
1604
  sawTerminal = await this.pumpStream(
1596
1605
  stream,
1597
1606
  conversationId,
1598
1607
  messageId,
1599
- executeClientTools
1608
+ executeClientTools,
1609
+ () => attemptController.abort()
1600
1610
  );
1601
1611
  } catch (err) {
1602
1612
  if (signal?.aborted) return;
@@ -1606,6 +1616,8 @@ var ChatSession = class {
1606
1616
  if (attempt >= SSE_MAX_RECONNECTS) throw err;
1607
1617
  await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);
1608
1618
  continue;
1619
+ } finally {
1620
+ signal?.removeEventListener("abort", linkAbort);
1609
1621
  }
1610
1622
  if (sawTerminal || signal?.aborted) return;
1611
1623
  if (attempt >= SSE_MAX_RECONNECTS) {
@@ -1618,35 +1630,66 @@ var ChatSession = class {
1618
1630
  * Consume a single SSE stream to exhaustion. Returns whether a terminal
1619
1631
  * event (``message_stop`` / ``error``) was seen, so the caller can decide
1620
1632
  * whether an ended stream means "turn done" vs "dropped, reconnect".
1633
+ *
1634
+ * ``onStall`` aborts the per-attempt connection: if no event arrives within
1635
+ * SSE_STALL_TIMEOUT_MS (backend keepalives land every 15s), the stream is a
1636
+ * zombie — ``reader.read()`` will never settle — so we kill the fetch and
1637
+ * throw a ConnectionError, feeding the caller's reconnect-from-lastSeq loop.
1621
1638
  */
1622
- async pumpStream(stream, conversationId, messageId, executeClientTools) {
1639
+ async pumpStream(stream, conversationId, messageId, executeClientTools, onStall) {
1623
1640
  let sawTerminal = false;
1624
- for await (const raw of stream) {
1625
- let parsed;
1626
- try {
1627
- const data = JSON.parse(raw.data);
1628
- if (typeof data !== "object" || data === null || typeof data.type !== "string") {
1629
- if (typeof data?.seq === "number") {
1641
+ const iterator = stream[Symbol.asyncIterator]();
1642
+ try {
1643
+ while (true) {
1644
+ const next = iterator.next();
1645
+ let stallTimer;
1646
+ const stall = new Promise((_, reject) => {
1647
+ stallTimer = setTimeout(() => {
1648
+ reject(
1649
+ new ConnectionError(
1650
+ `Stream stalled: no events for ${SSE_STALL_TIMEOUT_MS}ms`
1651
+ )
1652
+ );
1653
+ onStall?.();
1654
+ }, SSE_STALL_TIMEOUT_MS);
1655
+ });
1656
+ let result;
1657
+ try {
1658
+ result = await Promise.race([next, stall]);
1659
+ } finally {
1660
+ clearTimeout(stallTimer);
1661
+ }
1662
+ if (result.done) break;
1663
+ const raw = result.value;
1664
+ let parsed;
1665
+ try {
1666
+ const data = JSON.parse(raw.data);
1667
+ if (typeof data !== "object" || data === null || typeof data.type !== "string") {
1668
+ if (typeof data?.seq === "number") {
1669
+ this.lastSeq = data.seq;
1670
+ }
1671
+ continue;
1672
+ }
1673
+ parsed = data;
1674
+ if (typeof data.seq === "number") {
1630
1675
  this.lastSeq = data.seq;
1631
1676
  }
1677
+ } catch {
1632
1678
  continue;
1633
1679
  }
1634
- parsed = data;
1635
- if (typeof data.seq === "number") {
1636
- this.lastSeq = data.seq;
1680
+ if (parsed.type === "message_stop" || parsed.type === "error") {
1681
+ sawTerminal = true;
1637
1682
  }
1638
- } catch {
1639
- continue;
1640
- }
1641
- if (parsed.type === "message_stop" || parsed.type === "error") {
1642
- sawTerminal = true;
1683
+ await this.dispatchWireEvent(
1684
+ parsed,
1685
+ conversationId,
1686
+ messageId,
1687
+ executeClientTools
1688
+ );
1643
1689
  }
1644
- await this.dispatchWireEvent(
1645
- parsed,
1646
- conversationId,
1647
- messageId,
1648
- executeClientTools
1649
- );
1690
+ } finally {
1691
+ void iterator.return?.(void 0).catch(() => {
1692
+ });
1650
1693
  }
1651
1694
  return sawTerminal;
1652
1695
  }