@cadraos/sdk 0.3.12 → 0.3.14-p46.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.
@@ -447,7 +447,8 @@ var SSEClient = class {
447
447
  "Authorization": this.config.authHeader,
448
448
  "X-Org-Id": String(this.config.orgId),
449
449
  "Accept": "text/event-stream",
450
- "Cache-Control": "no-cache"
450
+ "Cache-Control": "no-cache",
451
+ ...this.config.lastEventId ? { "Last-Event-ID": this.config.lastEventId } : {}
451
452
  },
452
453
  signal: this.abortController.signal
453
454
  });
@@ -673,13 +674,167 @@ function normalizeUrl(url) {
673
674
  return url;
674
675
  }
675
676
 
677
+ // src/streaming/poll-policy.ts
678
+ var UNNAMED_RATE_LIMIT_WAIT_MS = 6e4;
679
+ function asDict(value) {
680
+ return typeof value === "object" && value !== null ? value : null;
681
+ }
682
+ var RATE_LIMIT_CODES = /* @__PURE__ */ new Set([
683
+ "rate_limit_error",
684
+ "rate_limited",
685
+ "rate_limit_exceeded",
686
+ "too_many_requests"
687
+ ]);
688
+ function isRateLimited(error) {
689
+ if (typeof error === "string") return /\b429\b/.test(error);
690
+ const err = asDict(error);
691
+ if (!err) return false;
692
+ for (const key of ["status", "statusCode", "httpStatus", "code"]) {
693
+ if (err[key] === 429 || err[key] === "429") return true;
694
+ }
695
+ const response = asDict(err.response);
696
+ if (response && (response.status === 429 || response.statusCode === 429)) return true;
697
+ if (typeof err.code === "string" && RATE_LIMIT_CODES.has(err.code.toLowerCase())) return true;
698
+ if (err.name === "RateLimitError") return true;
699
+ const message = typeof err.message === "string" ? err.message : "";
700
+ return /\b429\b/.test(message) || /rate[\s_-]?limit/i.test(message) || /too many requests/i.test(message);
701
+ }
702
+ function readHeader(bag, name) {
703
+ if (bag === null || bag === void 0) return null;
704
+ const getter = bag.get;
705
+ if (typeof getter === "function") {
706
+ try {
707
+ const value = getter.call(bag, name);
708
+ if (value !== null && value !== void 0) return String(value);
709
+ } catch {
710
+ }
711
+ }
712
+ const dict = asDict(bag);
713
+ if (!dict) return null;
714
+ const wanted = name.toLowerCase();
715
+ for (const [key, value] of Object.entries(dict)) {
716
+ if (key.toLowerCase() === wanted && value !== null && value !== void 0) {
717
+ return String(value);
718
+ }
719
+ }
720
+ return null;
721
+ }
722
+ function parseRetryAfter(raw, now) {
723
+ if (raw === null || raw === void 0) return null;
724
+ if (typeof raw === "number") {
725
+ return Number.isFinite(raw) && raw >= 0 ? raw * 1e3 : null;
726
+ }
727
+ if (typeof raw !== "string") return null;
728
+ const trimmed = raw.trim();
729
+ if (trimmed === "") return null;
730
+ if (/^\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed) * 1e3;
731
+ const at = Date.parse(trimmed);
732
+ return Number.isNaN(at) ? null : Math.max(0, at - now);
733
+ }
734
+ function parseResetAt(raw, now) {
735
+ if (raw === null || raw === void 0) return null;
736
+ const numeric = typeof raw === "number" ? raw : typeof raw === "string" && /^\d+(\.\d+)?$/.test(raw.trim()) ? Number(raw.trim()) : null;
737
+ if (numeric !== null) {
738
+ if (!Number.isFinite(numeric) || numeric < 0) return null;
739
+ if (numeric > 1e11) return Math.max(0, numeric - now);
740
+ if (numeric > 1e9) return Math.max(0, numeric * 1e3 - now);
741
+ return numeric * 1e3;
742
+ }
743
+ if (typeof raw !== "string") return null;
744
+ const at = Date.parse(raw.trim());
745
+ return Number.isNaN(at) ? null : Math.max(0, at - now);
746
+ }
747
+ function getRetryAfterMs(error, now = Date.now()) {
748
+ const err = asDict(error);
749
+ if (!err) return null;
750
+ const details = asDict(err.details);
751
+ const response = asDict(err.response);
752
+ const body = asDict(err.body) ?? asDict(response?.data);
753
+ const bodyError = asDict(body?.error);
754
+ const bodyDetails = asDict(bodyError?.details) ?? asDict(body?.details);
755
+ for (const source of [err, details, bodyDetails]) {
756
+ const ms = source?.retryAfterMs;
757
+ if (typeof ms === "number" && Number.isFinite(ms) && ms >= 0) return ms;
758
+ }
759
+ for (const source of [err, details, bodyDetails, bodyError]) {
760
+ const parsed = parseRetryAfter(source?.retryAfter, now);
761
+ if (parsed !== null) return parsed;
762
+ }
763
+ const headerBags = [err.headers, response?.headers, details?.headers, body?.headers];
764
+ for (const bag of headerBags) {
765
+ const parsed = parseRetryAfter(readHeader(bag, "retry-after"), now);
766
+ if (parsed !== null) return parsed;
767
+ }
768
+ for (const source of [details, bodyDetails, err]) {
769
+ for (const key of ["resetAt", "reset", "rateLimitReset"]) {
770
+ const parsed = parseResetAt(source?.[key], now);
771
+ if (parsed !== null) return parsed;
772
+ }
773
+ }
774
+ for (const bag of headerBags) {
775
+ for (const header of ["x-ratelimit-reset", "ratelimit-reset"]) {
776
+ const parsed = parseResetAt(readHeader(bag, header), now);
777
+ if (parsed !== null) return parsed;
778
+ }
779
+ }
780
+ return null;
781
+ }
782
+ function getHttpStatus(error) {
783
+ const err = asDict(error);
784
+ if (!err) return null;
785
+ for (const key of ["status", "statusCode", "httpStatus"]) {
786
+ const value = err[key];
787
+ if (typeof value === "number" && Number.isFinite(value)) return value;
788
+ if (typeof value === "string" && /^\d{3}$/.test(value)) return Number(value);
789
+ }
790
+ const response = asDict(err.response);
791
+ for (const key of ["status", "statusCode"]) {
792
+ const value = response?.[key];
793
+ if (typeof value === "number" && Number.isFinite(value)) return value;
794
+ }
795
+ return null;
796
+ }
797
+ function isTerminalClientError(error) {
798
+ const status = getHttpStatus(error);
799
+ if (status === null) return false;
800
+ return status >= 400 && status < 500 && status !== 429 && status !== 408;
801
+ }
802
+ var FAILURE_GROWTH_FACTOR = 2;
803
+ var IDLE_GROWTH_FACTOR = 1.5;
804
+ function nextFailureDelayMs(currentMs, maxMs) {
805
+ return Math.min(Math.max(currentMs, 1) * FAILURE_GROWTH_FACTOR, maxMs);
806
+ }
807
+ function nextIdleDelayMs(currentMs, maxMs) {
808
+ return Math.min(Math.ceil(Math.max(currentMs, 1) * IDLE_GROWTH_FACTOR), maxMs);
809
+ }
810
+ function applyJitter(delayMs, ratio, random = Math.random) {
811
+ if (ratio <= 0) return delayMs;
812
+ return Math.round(delayMs * (1 + random() * ratio));
813
+ }
814
+
676
815
  // src/streaming/stream-controller.ts
677
816
  var DEFAULT_STREAMING_CONFIG = {
678
817
  maxReconnectAttempts: 5,
679
818
  reconnectBaseDelayMs: 1e3,
680
819
  reconnectMaxDelayMs: 1e4,
820
+ // NOTE (unchanged, deliberately): this watchdog force-disconnects a HEALTHY
821
+ // SSE connection after 30s of silence and routes it through the same
822
+ // handleConnectionError path as a real transport failure. Ordinary LLM
823
+ // latency exceeds 30s routinely, so it fires against working streams — that
824
+ // is what drives sessions into the polling fallback in the first place. The
825
+ // fix is a server-side SSE heartbeat, not a bigger number here; raising this
826
+ // is a guess against a moving target. Everything below only bounds what the
827
+ // fallback costs once it has been entered.
681
828
  activityTimeoutMs: 3e4,
682
- pollingIntervalMs: 2e3
829
+ pollingIntervalMs: 5e3,
830
+ pollingMaxIntervalMs: 3e4,
831
+ pollingMaxConsecutiveFailures: 10,
832
+ pollingMaxConsecutiveRateLimits: 3,
833
+ pollingMaxConsecutiveClientErrors: 3,
834
+ pollingMaxDurationMs: 18e5,
835
+ pollingMaxRetryAfterMs: 3e5,
836
+ pollingJitterRatio: 0.1,
837
+ sseRetryIntervalMs: 6e4
683
838
  };
684
839
  var DEFAULT_POLLING_ENABLED = true;
685
840
  var ExecutionStreamControllerImpl = class {
@@ -692,9 +847,24 @@ var ExecutionStreamControllerImpl = class {
692
847
  seenEventIds = /* @__PURE__ */ new Set();
693
848
  _lastEventId = null;
694
849
  activityTimeoutId = null;
695
- pollingIntervalId = null;
696
850
  finalExecution = null;
697
851
  finalError = null;
852
+ // --- Fallback polling state ---
853
+ // A self-rescheduling timeout, not setInterval: the delay has to change
854
+ // between ticks (that is what backoff IS), and a timeout chain also cannot
855
+ // stack overlapping requests the way a fixed interval does when the API is
856
+ // slow to answer.
857
+ pollTimeoutId = null;
858
+ pollDelayMs = 0;
859
+ pollConsecutiveFailures = 0;
860
+ pollConsecutiveRateLimits = 0;
861
+ pollConsecutiveClientErrors = 0;
862
+ /** When the loop last observed anything new — the clock the give-up bound runs on. */
863
+ pollLastProgressAt = 0;
864
+ pollInFlight = false;
865
+ lastPolledStatus = null;
866
+ sseRetryTimeoutId = null;
867
+ sseRetryInFlight = false;
698
868
  // Event callbacks
699
869
  listeners = {
700
870
  log: [],
@@ -723,6 +893,39 @@ var ExecutionStreamControllerImpl = class {
723
893
  this.config = { ...DEFAULT_STREAMING_CONFIG, ...options.config };
724
894
  this.pollingEnabled = options.pollingEnabled ?? DEFAULT_POLLING_ENABLED;
725
895
  }
896
+ // Resolved fallback-poll settings. Read through `??` rather than off the
897
+ // spread config so an explicit `undefined` in a caller's partial config still
898
+ // lands on the default instead of NaN-ing the arithmetic.
899
+ get pollBaseIntervalMs() {
900
+ return this.config.pollingIntervalMs ?? DEFAULT_STREAMING_CONFIG.pollingIntervalMs;
901
+ }
902
+ get pollMaxIntervalMs() {
903
+ return Math.max(
904
+ this.config.pollingMaxIntervalMs ?? DEFAULT_STREAMING_CONFIG.pollingMaxIntervalMs,
905
+ this.pollBaseIntervalMs
906
+ );
907
+ }
908
+ get pollMaxConsecutiveFailures() {
909
+ return this.config.pollingMaxConsecutiveFailures ?? DEFAULT_STREAMING_CONFIG.pollingMaxConsecutiveFailures;
910
+ }
911
+ get pollMaxConsecutiveRateLimits() {
912
+ return this.config.pollingMaxConsecutiveRateLimits ?? DEFAULT_STREAMING_CONFIG.pollingMaxConsecutiveRateLimits;
913
+ }
914
+ get pollMaxConsecutiveClientErrors() {
915
+ return this.config.pollingMaxConsecutiveClientErrors ?? DEFAULT_STREAMING_CONFIG.pollingMaxConsecutiveClientErrors;
916
+ }
917
+ get pollMaxDurationMs() {
918
+ return this.config.pollingMaxDurationMs ?? DEFAULT_STREAMING_CONFIG.pollingMaxDurationMs;
919
+ }
920
+ get pollMaxRetryAfterMs() {
921
+ return this.config.pollingMaxRetryAfterMs ?? DEFAULT_STREAMING_CONFIG.pollingMaxRetryAfterMs;
922
+ }
923
+ get pollJitterRatio() {
924
+ return this.config.pollingJitterRatio ?? DEFAULT_STREAMING_CONFIG.pollingJitterRatio;
925
+ }
926
+ get sseRetryIntervalMs() {
927
+ return this.config.sseRetryIntervalMs ?? DEFAULT_STREAMING_CONFIG.sseRetryIntervalMs;
928
+ }
726
929
  /**
727
930
  * Get the last event ID (for resume support)
728
931
  */
@@ -813,28 +1016,45 @@ var ExecutionStreamControllerImpl = class {
813
1016
  console.log("[SDK:StreamController] executionId:", this.options.executionId);
814
1017
  console.log("[SDK:StreamController] orgId:", this.options.orgId);
815
1018
  console.log("[SDK:StreamController] apiKey:", authToken ? `${authToken.substring(0, 20)}...` : "MISSING");
816
- this.sseClient = new SSEClient({
1019
+ if (this.sseClient) {
1020
+ this.sseClient.disconnect();
1021
+ this.sseClient = null;
1022
+ }
1023
+ const client = new SSEClient({
817
1024
  url,
818
1025
  authHeader: `Bearer ${authToken}`,
819
1026
  orgId: this.options.orgId,
820
1027
  fetch: this.options.fetch,
821
- onEvent: (event) => this.handleEvent(event),
1028
+ // Resume from where this session left off. On a first connect this is
1029
+ // null and the header is omitted.
1030
+ lastEventId: this._lastEventId,
1031
+ onEvent: (event) => {
1032
+ if (this.sseClient !== client) return;
1033
+ this.handleEvent(event);
1034
+ },
822
1035
  onError: (error) => {
1036
+ if (this.sseClient !== client) return;
823
1037
  console.error("[SDK:StreamController] SSE error:", error);
824
1038
  this.handleConnectionError(error);
825
1039
  },
826
1040
  onConnectionChange: (connected) => {
1041
+ if (this.sseClient !== client) return;
827
1042
  console.log("[SDK:StreamController] SSE connection change:", connected);
828
1043
  if (connected) {
829
1044
  this.status = "connected";
830
1045
  this.reconnectAttempts = 0;
1046
+ this.stopPolling();
831
1047
  this.resetActivityTimeout();
832
1048
  }
833
- this.emit("connectionChange", { connected, usingFallback: false });
1049
+ this.emit("connectionChange", {
1050
+ connected,
1051
+ usingFallback: this.status === "fallback_polling"
1052
+ });
834
1053
  }
835
1054
  });
1055
+ this.sseClient = client;
836
1056
  try {
837
- await this.sseClient.connect();
1057
+ await client.connect();
838
1058
  console.log("[SDK:StreamController] SSE connect() completed");
839
1059
  } catch (error) {
840
1060
  console.error("[SDK:StreamController] SSE connect() threw:", error);
@@ -1073,6 +1293,7 @@ var ExecutionStreamControllerImpl = class {
1073
1293
  handleComplete(data) {
1074
1294
  this.status = "closed";
1075
1295
  this.stopActivityTimeout();
1296
+ this.stopPolling();
1076
1297
  const sessionId = data.sessionId ?? this.options.sessionId;
1077
1298
  const execution = {
1078
1299
  uuid: this.options.executionId,
@@ -1099,6 +1320,7 @@ var ExecutionStreamControllerImpl = class {
1099
1320
  handleError(error) {
1100
1321
  this.status = "closed";
1101
1322
  this.stopActivityTimeout();
1323
+ this.stopPolling();
1102
1324
  this.finalError = error;
1103
1325
  this.emit("error", error);
1104
1326
  if (this.waitReject) {
@@ -1125,6 +1347,10 @@ var ExecutionStreamControllerImpl = class {
1125
1347
  this.handleError({ error: msg, errorCode: "stream_unrecoverable" });
1126
1348
  return;
1127
1349
  }
1350
+ if (this.status === "fallback_polling") {
1351
+ this.startPolling();
1352
+ return;
1353
+ }
1128
1354
  if (this.reconnectAttempts < this.config.maxReconnectAttempts) {
1129
1355
  this.status = "reconnecting";
1130
1356
  this.reconnectAttempts++;
@@ -1194,41 +1420,215 @@ var ExecutionStreamControllerImpl = class {
1194
1420
  }
1195
1421
  }
1196
1422
  /**
1197
- * Start polling fallback
1423
+ * Start polling fallback.
1424
+ *
1425
+ * Idempotent — calling it while a loop is already running is a no-op, so no
1426
+ * path can end up with two loops racing the same execution.
1198
1427
  */
1199
1428
  startPolling() {
1200
- this.pollingIntervalId = setInterval(async () => {
1201
- try {
1202
- const execution = await this.options.getExecution(this.options.executionId);
1203
- for (const log of execution.logs) {
1204
- if (!this.seenEventIds.has(log.uuid)) {
1205
- this.seenEventIds.add(log.uuid);
1206
- this.emit("log", log);
1207
- }
1208
- }
1209
- if (execution.status === "completed") {
1210
- this.stopPolling();
1211
- this.handleComplete(execution);
1212
- } else if (execution.status === "failed" || execution.status === "cancelled") {
1213
- this.stopPolling();
1214
- this.handleError({
1215
- error: execution.error ?? `Execution ${execution.status}`,
1216
- errorCode: execution.status
1217
- });
1429
+ if (this.pollTimeoutId !== null || this.pollInFlight) {
1430
+ return;
1431
+ }
1432
+ this.pollLastProgressAt = Date.now();
1433
+ this.pollDelayMs = this.pollBaseIntervalMs;
1434
+ this.pollConsecutiveFailures = 0;
1435
+ this.pollConsecutiveRateLimits = 0;
1436
+ this.pollConsecutiveClientErrors = 0;
1437
+ this.lastPolledStatus = null;
1438
+ this.stopActivityTimeout();
1439
+ this.scheduleSSERetry();
1440
+ this.schedulePoll(this.pollDelayMs);
1441
+ }
1442
+ /**
1443
+ * Queue the next poll tick.
1444
+ */
1445
+ schedulePoll(delayMs) {
1446
+ if (this.status !== "fallback_polling") {
1447
+ return;
1448
+ }
1449
+ this.pollTimeoutId = setTimeout(() => {
1450
+ this.pollTimeoutId = null;
1451
+ void this.runPoll();
1452
+ }, Math.max(0, Math.round(delayMs)));
1453
+ }
1454
+ /**
1455
+ * One poll tick: fetch state, emit what is new, then decide how long to wait
1456
+ * before the next one — or whether there should be a next one at all.
1457
+ */
1458
+ async runPoll() {
1459
+ if (this.status !== "fallback_polling") {
1460
+ return;
1461
+ }
1462
+ if (Date.now() - this.pollLastProgressAt >= this.pollMaxDurationMs) {
1463
+ this.failPolling(
1464
+ `Streaming fallback gave up: no execution progress for ${Math.round(this.pollMaxDurationMs / 1e3)}s`,
1465
+ "polling_timeout"
1466
+ );
1467
+ return;
1468
+ }
1469
+ this.pollInFlight = true;
1470
+ try {
1471
+ const execution = await this.options.getExecution(this.options.executionId);
1472
+ this.pollConsecutiveFailures = 0;
1473
+ this.pollConsecutiveRateLimits = 0;
1474
+ this.pollConsecutiveClientErrors = 0;
1475
+ let sawNewInfo = false;
1476
+ for (const log of execution.logs ?? []) {
1477
+ if (!this.seenEventIds.has(log.uuid)) {
1478
+ this.seenEventIds.add(log.uuid);
1479
+ this.emit("log", log);
1480
+ sawNewInfo = true;
1218
1481
  }
1219
- } catch (error) {
1220
- console.error("Polling error:", error);
1221
1482
  }
1222
- }, this.config.pollingIntervalMs);
1483
+ if (execution.status !== this.lastPolledStatus) {
1484
+ this.lastPolledStatus = execution.status;
1485
+ sawNewInfo = true;
1486
+ }
1487
+ if (sawNewInfo) {
1488
+ this.pollLastProgressAt = Date.now();
1489
+ }
1490
+ if (execution.status === "completed") {
1491
+ this.stopPolling();
1492
+ this.handleComplete(execution);
1493
+ return;
1494
+ }
1495
+ if (execution.status === "failed" || execution.status === "cancelled") {
1496
+ this.stopPolling();
1497
+ this.handleError({
1498
+ error: execution.error ?? `Execution ${execution.status}`,
1499
+ errorCode: execution.status
1500
+ });
1501
+ return;
1502
+ }
1503
+ this.pollDelayMs = sawNewInfo ? this.pollBaseIntervalMs : nextIdleDelayMs(this.pollDelayMs, this.pollMaxIntervalMs);
1504
+ } catch (error) {
1505
+ if (!this.handlePollError(error)) {
1506
+ return;
1507
+ }
1508
+ } finally {
1509
+ this.pollInFlight = false;
1510
+ }
1511
+ this.schedulePoll(applyJitter(this.pollDelayMs, this.pollJitterRatio));
1512
+ }
1513
+ /**
1514
+ * Apply backpressure for a failed poll.
1515
+ *
1516
+ * @returns `true` to keep polling (with `pollDelayMs` updated), `false` when
1517
+ * the loop has been terminated.
1518
+ */
1519
+ handlePollError(error) {
1520
+ const retryAfterMs = getRetryAfterMs(error);
1521
+ const rateLimited = isRateLimited(error) || retryAfterMs !== null;
1522
+ if (rateLimited) {
1523
+ this.pollConsecutiveRateLimits++;
1524
+ console.warn(
1525
+ "[SDK:StreamController] Poll rate-limited",
1526
+ retryAfterMs !== null ? `(server asked for ${Math.round(retryAfterMs / 1e3)}s)` : "(no reset named)"
1527
+ );
1528
+ if (retryAfterMs !== null && retryAfterMs > this.pollMaxRetryAfterMs) {
1529
+ this.failPolling(
1530
+ `Rate limited: the API asked for ${Math.round(retryAfterMs / 1e3)}s, longer than this stream will wait`,
1531
+ "rate_limited"
1532
+ );
1533
+ return false;
1534
+ }
1535
+ if (this.pollConsecutiveRateLimits >= this.pollMaxConsecutiveRateLimits) {
1536
+ this.failPolling(
1537
+ "Rate limited by the API; stopped the streaming fallback rather than keep spending the key quota",
1538
+ "rate_limited"
1539
+ );
1540
+ return false;
1541
+ }
1542
+ this.pollDelayMs = Math.max(retryAfterMs ?? UNNAMED_RATE_LIMIT_WAIT_MS, this.pollDelayMs);
1543
+ return true;
1544
+ }
1545
+ if (isTerminalClientError(error)) {
1546
+ this.pollConsecutiveClientErrors++;
1547
+ const status = getHttpStatus(error);
1548
+ console.warn(
1549
+ `[SDK:StreamController] Poll got HTTP ${status} (${this.pollConsecutiveClientErrors}/${this.pollMaxConsecutiveClientErrors})`
1550
+ );
1551
+ if (this.pollConsecutiveClientErrors >= this.pollMaxConsecutiveClientErrors) {
1552
+ this.failPolling(
1553
+ `Execution status endpoint returned HTTP ${status} ${this.pollConsecutiveClientErrors} times \u2014 cannot track this run`,
1554
+ "polling_unrecoverable"
1555
+ );
1556
+ return false;
1557
+ }
1558
+ this.pollDelayMs = nextFailureDelayMs(this.pollDelayMs, this.pollMaxIntervalMs);
1559
+ return true;
1560
+ }
1561
+ this.pollConsecutiveFailures++;
1562
+ console.warn(
1563
+ `[SDK:StreamController] Poll failed (${this.pollConsecutiveFailures}/${this.pollMaxConsecutiveFailures}):`,
1564
+ error
1565
+ );
1566
+ if (this.pollConsecutiveFailures >= this.pollMaxConsecutiveFailures) {
1567
+ this.failPolling(
1568
+ `Streaming fallback failed ${this.pollConsecutiveFailures} times in a row`,
1569
+ "polling_failed"
1570
+ );
1571
+ return false;
1572
+ }
1573
+ this.pollDelayMs = nextFailureDelayMs(this.pollDelayMs, this.pollMaxIntervalMs);
1574
+ return true;
1575
+ }
1576
+ /**
1577
+ * Terminate the fallback loop and surface the reason.
1578
+ */
1579
+ failPolling(error, errorCode) {
1580
+ this.stopPolling();
1581
+ this.handleError({ error, errorCode });
1223
1582
  }
1224
1583
  /**
1225
- * Stop polling
1584
+ * Stop polling, including the periodic SSE re-attempt that belongs to it.
1226
1585
  */
1227
1586
  stopPolling() {
1228
- if (this.pollingIntervalId) {
1229
- clearInterval(this.pollingIntervalId);
1230
- this.pollingIntervalId = null;
1587
+ if (this.pollTimeoutId !== null) {
1588
+ clearTimeout(this.pollTimeoutId);
1589
+ this.pollTimeoutId = null;
1590
+ }
1591
+ if (this.sseRetryTimeoutId !== null) {
1592
+ clearTimeout(this.sseRetryTimeoutId);
1593
+ this.sseRetryTimeoutId = null;
1594
+ }
1595
+ }
1596
+ /**
1597
+ * Queue the next attempt to get the real stream back.
1598
+ */
1599
+ scheduleSSERetry() {
1600
+ if (this.sseRetryTimeoutId !== null || this.status !== "fallback_polling") {
1601
+ return;
1602
+ }
1603
+ this.sseRetryTimeoutId = setTimeout(() => {
1604
+ this.sseRetryTimeoutId = null;
1605
+ this.attemptSSERecovery();
1606
+ }, this.sseRetryIntervalMs);
1607
+ }
1608
+ /**
1609
+ * Re-attempt SSE from inside the fallback, so a transient outage costs the
1610
+ * session a few polls rather than its stream for the lifetime of the tab.
1611
+ *
1612
+ * Success is NOT observed here: `SSEClient.connect()` resolves when the
1613
+ * stream ENDS, not when it opens. The open is observed by
1614
+ * `onConnectionChange(true)`, which flips status and stops this loop. The
1615
+ * promise below only tells us the attempt is over so the next one can be
1616
+ * queued.
1617
+ */
1618
+ attemptSSERecovery() {
1619
+ if (this.status !== "fallback_polling") {
1620
+ return;
1621
+ }
1622
+ if (this.sseRetryInFlight) {
1623
+ this.scheduleSSERetry();
1624
+ return;
1231
1625
  }
1626
+ this.sseRetryInFlight = true;
1627
+ void this.connectSSE().catch(() => {
1628
+ }).finally(() => {
1629
+ this.sseRetryInFlight = false;
1630
+ this.scheduleSSERetry();
1631
+ });
1232
1632
  }
1233
1633
  /**
1234
1634
  * Reset activity timeout
@@ -1360,12 +1760,23 @@ async function collectImagesFromStream(stream, options = {}) {
1360
1760
  });
1361
1761
  }
1362
1762
 
1763
+ // src/utils/api-paths.ts
1764
+ var API_V1 = "/api/v1";
1765
+ var EXECUTION_PATH = `${API_V1}/executions`;
1766
+ function executionPath(executionId) {
1767
+ return `${EXECUTION_PATH}/${executionId}`;
1768
+ }
1769
+ function executionCancelPath(executionId) {
1770
+ return `${EXECUTION_PATH}/${executionId}/cancel`;
1771
+ }
1772
+
1363
1773
  // src/resources/agents.ts
1364
1774
  var AgentsAPI = class extends BaseResourceAPI {
1365
1775
  constructor(http, getStreamConfig) {
1366
1776
  super(http, "/api/v1/agents");
1367
1777
  this.getStreamConfig = getStreamConfig;
1368
1778
  }
1779
+ getStreamConfig;
1369
1780
  // ===== LIST Operations =====
1370
1781
  /**
1371
1782
  * List all agents in the organization
@@ -1622,7 +2033,7 @@ var AgentsAPI = class extends BaseResourceAPI {
1622
2033
  * ```
1623
2034
  */
1624
2035
  async getExecution(executionId) {
1625
- return this.http.get(`/executions/${executionId}`);
2036
+ return this.http.get(executionPath(executionId));
1626
2037
  }
1627
2038
  // ===== EDIT Operations =====
1628
2039
  /**
@@ -1677,7 +2088,7 @@ var AgentsAPI = class extends BaseResourceAPI {
1677
2088
  */
1678
2089
  async cancelExecution(executionId) {
1679
2090
  return this.http.post(
1680
- `/executions/${executionId}/cancel`,
2091
+ executionCancelPath(executionId),
1681
2092
  void 0,
1682
2093
  { skipRetry: true }
1683
2094
  );
@@ -2178,6 +2589,7 @@ var ArtifactsAPI = class extends BaseResourceAPI {
2178
2589
  }
2179
2590
  return this.http.request({
2180
2591
  method: "POST",
2592
+ // KNOWN BROKEN — same missing `/api/v1` prefix as listArtifacts above.
2181
2593
  path: `/executions/${executionId}/artifacts`,
2182
2594
  body: formData,
2183
2595
  headers: {
@@ -2271,6 +2683,7 @@ var SessionsAPI = class extends BaseResourceAPI {
2271
2683
  super(http, "/api/v1/sessions");
2272
2684
  this.getStreamConfig = getStreamConfig;
2273
2685
  }
2686
+ getStreamConfig;
2274
2687
  // ===== LIST Operations =====
2275
2688
  /**
2276
2689
  * List all sessions in the organization
@@ -2450,7 +2863,7 @@ var SessionsAPI = class extends BaseResourceAPI {
2450
2863
  * Get execution result
2451
2864
  */
2452
2865
  async getExecution(executionId) {
2453
- return this.http.get(`/executions/${executionId}`);
2866
+ return this.http.get(executionPath(executionId));
2454
2867
  }
2455
2868
  // ===== EDIT Operations =====
2456
2869
  /**
@@ -2590,6 +3003,7 @@ var KanbanAPI = class extends BaseResourceAPI {
2590
3003
  super(http, "/api/v1/kanban");
2591
3004
  this.getStreamConfig = getStreamConfig;
2592
3005
  }
3006
+ getStreamConfig;
2593
3007
  // ===== BOARD =====
2594
3008
  /**
2595
3009
  * Get the current org's board (lanes in declared order). Auto-provisions
@@ -2724,7 +3138,7 @@ var KanbanAPI = class extends BaseResourceAPI {
2724
3138
  * @returns Execution details.
2725
3139
  */
2726
3140
  async getCardRun(executionId) {
2727
- return this.http.get(`/api/v1/agents/executions/${executionId}`);
3141
+ return this.http.get(executionPath(executionId));
2728
3142
  }
2729
3143
  /**
2730
3144
  * Create a stream controller for a card's run, keyed on the card's
@@ -2791,6 +3205,7 @@ var RemoteAgentsAPI = class extends BaseResourceAPI {
2791
3205
  super(http, "/api/v1/remote-agents");
2792
3206
  this.getStreamConfig = getStreamConfig;
2793
3207
  }
3208
+ getStreamConfig;
2794
3209
  /**
2795
3210
  * List all remote agents in the organization
2796
3211
  *
@@ -2924,7 +3339,7 @@ var RemoteAgentsAPI = class extends BaseResourceAPI {
2924
3339
  apiKey: config.apiKey,
2925
3340
  tokenProvider: config.tokenProvider,
2926
3341
  orgId: config.orgId,
2927
- getExecution: (id) => this.http.get(`/executions/${id}`)
3342
+ getExecution: (id) => this.http.get(executionPath(id))
2928
3343
  });
2929
3344
  return controller;
2930
3345
  }
@@ -4005,6 +4420,13 @@ function initialState() {
4005
4420
 
4006
4421
  // src/chat/hooks/useAgentChat.ts
4007
4422
  var __DEV__ = typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
4423
+ var LEGACY_POLL_BASE_INTERVAL_MS = 5e3;
4424
+ var LEGACY_POLL_MAX_INTERVAL_MS = 3e4;
4425
+ var LEGACY_POLL_MAX_FAILURES = 10;
4426
+ var LEGACY_POLL_MAX_RATE_LIMITS = 3;
4427
+ var LEGACY_POLL_MAX_IDLE_MS = 18e5;
4428
+ var LEGACY_POLL_MAX_RETRY_AFTER_MS = 3e5;
4429
+ var LEGACY_POLL_JITTER_RATIO = 0.1;
4008
4430
  var SILENT_TOOL_NAMES = /* @__PURE__ */ new Set([
4009
4431
  "check_delegations"
4010
4432
  ]);
@@ -4592,12 +5014,17 @@ function useAgentChat(options) {
4592
5014
  }
4593
5015
  }
4594
5016
  });
4595
- const pollingIntervalRef = react.useRef(null);
5017
+ const pollingTimeoutRef = react.useRef(null);
4596
5018
  const pollingExecutionIdRef = react.useRef(null);
5019
+ const pollDelayRef = react.useRef(0);
5020
+ const pollFailuresRef = react.useRef(0);
5021
+ const pollRateLimitsRef = react.useRef(0);
5022
+ const pollLastProgressRef = react.useRef(0);
5023
+ const pollLastStatusRef = react.useRef(null);
4597
5024
  const stopPolling = react.useCallback(() => {
4598
- if (pollingIntervalRef.current) {
4599
- clearInterval(pollingIntervalRef.current);
4600
- pollingIntervalRef.current = null;
5025
+ if (pollingTimeoutRef.current) {
5026
+ clearTimeout(pollingTimeoutRef.current);
5027
+ pollingTimeoutRef.current = null;
4601
5028
  }
4602
5029
  pollingExecutionIdRef.current = null;
4603
5030
  }, []);
@@ -4605,26 +5032,77 @@ function useAgentChat(options) {
4605
5032
  if (pollingExecutionIdRef.current === executionId) return;
4606
5033
  stopPolling();
4607
5034
  pollingExecutionIdRef.current = executionId;
5035
+ const baseDelayMs = pollingInterval ?? LEGACY_POLL_BASE_INTERVAL_MS;
5036
+ pollDelayRef.current = baseDelayMs;
5037
+ pollFailuresRef.current = 0;
5038
+ pollRateLimitsRef.current = 0;
5039
+ pollLastProgressRef.current = Date.now();
5040
+ pollLastStatusRef.current = null;
4608
5041
  if (__DEV__) console.log("[useAgentChat] Starting polling fallback for:", executionId);
5042
+ const schedule = (delayMs) => {
5043
+ if (pollingExecutionIdRef.current !== executionId) return;
5044
+ pollingTimeoutRef.current = setTimeout(() => {
5045
+ pollingTimeoutRef.current = null;
5046
+ void poll();
5047
+ }, Math.max(0, Math.round(delayMs)));
5048
+ };
5049
+ const giveUp = (message) => {
5050
+ if (__DEV__) console.warn("[useAgentChat] Polling gave up:", message);
5051
+ stopPolling();
5052
+ handleError(message);
5053
+ };
4609
5054
  const poll = async () => {
4610
5055
  if (pollingExecutionIdRef.current !== executionId) return;
5056
+ if (Date.now() - pollLastProgressRef.current >= LEGACY_POLL_MAX_IDLE_MS) {
5057
+ giveUp("Lost the live connection and stopped receiving updates for this run");
5058
+ return;
5059
+ }
4611
5060
  try {
4612
5061
  const execution = await adapter.getExecution(executionId);
5062
+ pollFailuresRef.current = 0;
5063
+ pollRateLimitsRef.current = 0;
5064
+ if (execution.status !== pollLastStatusRef.current) {
5065
+ pollLastStatusRef.current = execution.status;
5066
+ pollLastProgressRef.current = Date.now();
5067
+ pollDelayRef.current = baseDelayMs;
5068
+ } else {
5069
+ pollDelayRef.current = nextIdleDelayMs(pollDelayRef.current, LEGACY_POLL_MAX_INTERVAL_MS);
5070
+ }
4613
5071
  if (execution.status === "completed") {
4614
5072
  if (__DEV__) console.log("[useAgentChat] Polling: execution completed");
4615
5073
  stopPolling();
4616
5074
  handleComplete(execution.result || void 0);
4617
- } else if (execution.status === "failed") {
5075
+ return;
5076
+ }
5077
+ if (execution.status === "failed") {
4618
5078
  if (__DEV__) console.log("[useAgentChat] Polling: execution failed");
4619
5079
  stopPolling();
4620
5080
  handleError(execution.error || "Execution failed");
5081
+ return;
4621
5082
  }
4622
5083
  } catch (err) {
4623
- if (__DEV__) console.warn("[useAgentChat] Polling error (will retry):", err);
5084
+ const retryAfterMs = getRetryAfterMs(err);
5085
+ if (isRateLimited(err) || retryAfterMs !== null) {
5086
+ pollRateLimitsRef.current++;
5087
+ const resetTooFarOut = retryAfterMs !== null && retryAfterMs > LEGACY_POLL_MAX_RETRY_AFTER_MS;
5088
+ if (resetTooFarOut || pollRateLimitsRef.current >= LEGACY_POLL_MAX_RATE_LIMITS) {
5089
+ giveUp("Rate limited by the API \u2014 stopped polling for updates on this run");
5090
+ return;
5091
+ }
5092
+ pollDelayRef.current = Math.max(retryAfterMs ?? UNNAMED_RATE_LIMIT_WAIT_MS, pollDelayRef.current);
5093
+ } else {
5094
+ pollFailuresRef.current++;
5095
+ if (__DEV__) console.warn("[useAgentChat] Polling error (will retry):", err);
5096
+ if (pollFailuresRef.current >= LEGACY_POLL_MAX_FAILURES) {
5097
+ giveUp("Lost connection to the agent \u2014 stopped polling for updates on this run");
5098
+ return;
5099
+ }
5100
+ pollDelayRef.current = nextFailureDelayMs(pollDelayRef.current, LEGACY_POLL_MAX_INTERVAL_MS);
5101
+ }
4624
5102
  }
5103
+ schedule(applyJitter(pollDelayRef.current, LEGACY_POLL_JITTER_RATIO));
4625
5104
  };
4626
- poll();
4627
- pollingIntervalRef.current = setInterval(poll, pollingInterval ?? 2e3);
5105
+ void poll();
4628
5106
  }, [adapter, stopPolling, handleComplete, handleError, pollingInterval]);
4629
5107
  react.useEffect(() => {
4630
5108
  if (!useStreamController && isUsingFallback && currentExecutionId && isLoading && pollingEnabled !== false) {