@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.d.cts CHANGED
@@ -1180,6 +1180,11 @@ declare class ChatSession {
1180
1180
  * Consume a single SSE stream to exhaustion. Returns whether a terminal
1181
1181
  * event (``message_stop`` / ``error``) was seen, so the caller can decide
1182
1182
  * whether an ended stream means "turn done" vs "dropped, reconnect".
1183
+ *
1184
+ * ``onStall`` aborts the per-attempt connection: if no event arrives within
1185
+ * SSE_STALL_TIMEOUT_MS (backend keepalives land every 15s), the stream is a
1186
+ * zombie — ``reader.read()`` will never settle — so we kill the fetch and
1187
+ * throw a ConnectionError, feeding the caller's reconnect-from-lastSeq loop.
1183
1188
  */
1184
1189
  private pumpStream;
1185
1190
  /** Sleep for ``ms``, resolving early if the turn is aborted mid-backoff. */
package/dist/index.d.ts CHANGED
@@ -1180,6 +1180,11 @@ declare class ChatSession {
1180
1180
  * Consume a single SSE stream to exhaustion. Returns whether a terminal
1181
1181
  * event (``message_stop`` / ``error``) was seen, so the caller can decide
1182
1182
  * whether an ended stream means "turn done" vs "dropped, reconnect".
1183
+ *
1184
+ * ``onStall`` aborts the per-attempt connection: if no event arrives within
1185
+ * SSE_STALL_TIMEOUT_MS (backend keepalives land every 15s), the stream is a
1186
+ * zombie — ``reader.read()`` will never settle — so we kill the fetch and
1187
+ * throw a ConnectionError, feeding the caller's reconnect-from-lastSeq loop.
1183
1188
  */
1184
1189
  private pumpStream;
1185
1190
  /** Sleep for ``ms``, resolving early if the turn is aborted mid-backoff. */
package/dist/index.js CHANGED
@@ -1291,6 +1291,7 @@ function translateWireEvent(wire) {
1291
1291
 
1292
1292
  // src/session.ts
1293
1293
  var SSE_MAX_RECONNECTS = 6;
1294
+ var SSE_STALL_TIMEOUT_MS = 45e3;
1294
1295
  var TOOL_RESULT_MAX_RETRIES = 3;
1295
1296
  var CONVERSATION_PAGE_SIZE = 50;
1296
1297
  function sseReconnectDelayMs(attempt) {
@@ -1542,14 +1543,23 @@ var ChatSession = class {
1542
1543
  async consumeEventStream(jobId, conversationId, messageId, executeClientTools) {
1543
1544
  const signal = this.abortController?.signal;
1544
1545
  for (let attempt = 0; ; attempt++) {
1545
- const stream = this.client.streamJobEvents(jobId, this.lastSeq, signal);
1546
+ if (signal?.aborted) return;
1547
+ const attemptController = new AbortController();
1548
+ const linkAbort = () => attemptController.abort();
1549
+ signal?.addEventListener("abort", linkAbort);
1546
1550
  let sawTerminal;
1547
1551
  try {
1552
+ const stream = this.client.streamJobEvents(
1553
+ jobId,
1554
+ this.lastSeq,
1555
+ attemptController.signal
1556
+ );
1548
1557
  sawTerminal = await this.pumpStream(
1549
1558
  stream,
1550
1559
  conversationId,
1551
1560
  messageId,
1552
- executeClientTools
1561
+ executeClientTools,
1562
+ () => attemptController.abort()
1553
1563
  );
1554
1564
  } catch (err) {
1555
1565
  if (signal?.aborted) return;
@@ -1559,6 +1569,8 @@ var ChatSession = class {
1559
1569
  if (attempt >= SSE_MAX_RECONNECTS) throw err;
1560
1570
  await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);
1561
1571
  continue;
1572
+ } finally {
1573
+ signal?.removeEventListener("abort", linkAbort);
1562
1574
  }
1563
1575
  if (sawTerminal || signal?.aborted) return;
1564
1576
  if (attempt >= SSE_MAX_RECONNECTS) {
@@ -1571,35 +1583,66 @@ var ChatSession = class {
1571
1583
  * Consume a single SSE stream to exhaustion. Returns whether a terminal
1572
1584
  * event (``message_stop`` / ``error``) was seen, so the caller can decide
1573
1585
  * whether an ended stream means "turn done" vs "dropped, reconnect".
1586
+ *
1587
+ * ``onStall`` aborts the per-attempt connection: if no event arrives within
1588
+ * SSE_STALL_TIMEOUT_MS (backend keepalives land every 15s), the stream is a
1589
+ * zombie — ``reader.read()`` will never settle — so we kill the fetch and
1590
+ * throw a ConnectionError, feeding the caller's reconnect-from-lastSeq loop.
1574
1591
  */
1575
- async pumpStream(stream, conversationId, messageId, executeClientTools) {
1592
+ async pumpStream(stream, conversationId, messageId, executeClientTools, onStall) {
1576
1593
  let sawTerminal = false;
1577
- for await (const raw of stream) {
1578
- let parsed;
1579
- try {
1580
- const data = JSON.parse(raw.data);
1581
- if (typeof data !== "object" || data === null || typeof data.type !== "string") {
1582
- if (typeof data?.seq === "number") {
1594
+ const iterator = stream[Symbol.asyncIterator]();
1595
+ try {
1596
+ while (true) {
1597
+ const next = iterator.next();
1598
+ let stallTimer;
1599
+ const stall = new Promise((_, reject) => {
1600
+ stallTimer = setTimeout(() => {
1601
+ reject(
1602
+ new ConnectionError(
1603
+ `Stream stalled: no events for ${SSE_STALL_TIMEOUT_MS}ms`
1604
+ )
1605
+ );
1606
+ onStall?.();
1607
+ }, SSE_STALL_TIMEOUT_MS);
1608
+ });
1609
+ let result;
1610
+ try {
1611
+ result = await Promise.race([next, stall]);
1612
+ } finally {
1613
+ clearTimeout(stallTimer);
1614
+ }
1615
+ if (result.done) break;
1616
+ const raw = result.value;
1617
+ let parsed;
1618
+ try {
1619
+ const data = JSON.parse(raw.data);
1620
+ if (typeof data !== "object" || data === null || typeof data.type !== "string") {
1621
+ if (typeof data?.seq === "number") {
1622
+ this.lastSeq = data.seq;
1623
+ }
1624
+ continue;
1625
+ }
1626
+ parsed = data;
1627
+ if (typeof data.seq === "number") {
1583
1628
  this.lastSeq = data.seq;
1584
1629
  }
1630
+ } catch {
1585
1631
  continue;
1586
1632
  }
1587
- parsed = data;
1588
- if (typeof data.seq === "number") {
1589
- this.lastSeq = data.seq;
1633
+ if (parsed.type === "message_stop" || parsed.type === "error") {
1634
+ sawTerminal = true;
1590
1635
  }
1591
- } catch {
1592
- continue;
1593
- }
1594
- if (parsed.type === "message_stop" || parsed.type === "error") {
1595
- sawTerminal = true;
1636
+ await this.dispatchWireEvent(
1637
+ parsed,
1638
+ conversationId,
1639
+ messageId,
1640
+ executeClientTools
1641
+ );
1596
1642
  }
1597
- await this.dispatchWireEvent(
1598
- parsed,
1599
- conversationId,
1600
- messageId,
1601
- executeClientTools
1602
- );
1643
+ } finally {
1644
+ void iterator.return?.(void 0).catch(() => {
1645
+ });
1603
1646
  }
1604
1647
  return sawTerminal;
1605
1648
  }