@cadraos/sdk 0.3.11 → 0.3.13

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.
@@ -440,7 +440,8 @@ var SSEClient = class {
440
440
  "Authorization": this.config.authHeader,
441
441
  "X-Org-Id": String(this.config.orgId),
442
442
  "Accept": "text/event-stream",
443
- "Cache-Control": "no-cache"
443
+ "Cache-Control": "no-cache",
444
+ ...this.config.lastEventId ? { "Last-Event-ID": this.config.lastEventId } : {}
444
445
  },
445
446
  signal: this.abortController.signal
446
447
  });
@@ -666,13 +667,167 @@ function normalizeUrl(url) {
666
667
  return url;
667
668
  }
668
669
 
670
+ // src/streaming/poll-policy.ts
671
+ var UNNAMED_RATE_LIMIT_WAIT_MS = 6e4;
672
+ function asDict(value) {
673
+ return typeof value === "object" && value !== null ? value : null;
674
+ }
675
+ var RATE_LIMIT_CODES = /* @__PURE__ */ new Set([
676
+ "rate_limit_error",
677
+ "rate_limited",
678
+ "rate_limit_exceeded",
679
+ "too_many_requests"
680
+ ]);
681
+ function isRateLimited(error) {
682
+ if (typeof error === "string") return /\b429\b/.test(error);
683
+ const err = asDict(error);
684
+ if (!err) return false;
685
+ for (const key of ["status", "statusCode", "httpStatus", "code"]) {
686
+ if (err[key] === 429 || err[key] === "429") return true;
687
+ }
688
+ const response = asDict(err.response);
689
+ if (response && (response.status === 429 || response.statusCode === 429)) return true;
690
+ if (typeof err.code === "string" && RATE_LIMIT_CODES.has(err.code.toLowerCase())) return true;
691
+ if (err.name === "RateLimitError") return true;
692
+ const message = typeof err.message === "string" ? err.message : "";
693
+ return /\b429\b/.test(message) || /rate[\s_-]?limit/i.test(message) || /too many requests/i.test(message);
694
+ }
695
+ function readHeader(bag, name) {
696
+ if (bag === null || bag === void 0) return null;
697
+ const getter = bag.get;
698
+ if (typeof getter === "function") {
699
+ try {
700
+ const value = getter.call(bag, name);
701
+ if (value !== null && value !== void 0) return String(value);
702
+ } catch {
703
+ }
704
+ }
705
+ const dict = asDict(bag);
706
+ if (!dict) return null;
707
+ const wanted = name.toLowerCase();
708
+ for (const [key, value] of Object.entries(dict)) {
709
+ if (key.toLowerCase() === wanted && value !== null && value !== void 0) {
710
+ return String(value);
711
+ }
712
+ }
713
+ return null;
714
+ }
715
+ function parseRetryAfter(raw, now) {
716
+ if (raw === null || raw === void 0) return null;
717
+ if (typeof raw === "number") {
718
+ return Number.isFinite(raw) && raw >= 0 ? raw * 1e3 : null;
719
+ }
720
+ if (typeof raw !== "string") return null;
721
+ const trimmed = raw.trim();
722
+ if (trimmed === "") return null;
723
+ if (/^\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed) * 1e3;
724
+ const at = Date.parse(trimmed);
725
+ return Number.isNaN(at) ? null : Math.max(0, at - now);
726
+ }
727
+ function parseResetAt(raw, now) {
728
+ if (raw === null || raw === void 0) return null;
729
+ const numeric = typeof raw === "number" ? raw : typeof raw === "string" && /^\d+(\.\d+)?$/.test(raw.trim()) ? Number(raw.trim()) : null;
730
+ if (numeric !== null) {
731
+ if (!Number.isFinite(numeric) || numeric < 0) return null;
732
+ if (numeric > 1e11) return Math.max(0, numeric - now);
733
+ if (numeric > 1e9) return Math.max(0, numeric * 1e3 - now);
734
+ return numeric * 1e3;
735
+ }
736
+ if (typeof raw !== "string") return null;
737
+ const at = Date.parse(raw.trim());
738
+ return Number.isNaN(at) ? null : Math.max(0, at - now);
739
+ }
740
+ function getRetryAfterMs(error, now = Date.now()) {
741
+ const err = asDict(error);
742
+ if (!err) return null;
743
+ const details = asDict(err.details);
744
+ const response = asDict(err.response);
745
+ const body = asDict(err.body) ?? asDict(response?.data);
746
+ const bodyError = asDict(body?.error);
747
+ const bodyDetails = asDict(bodyError?.details) ?? asDict(body?.details);
748
+ for (const source of [err, details, bodyDetails]) {
749
+ const ms = source?.retryAfterMs;
750
+ if (typeof ms === "number" && Number.isFinite(ms) && ms >= 0) return ms;
751
+ }
752
+ for (const source of [err, details, bodyDetails, bodyError]) {
753
+ const parsed = parseRetryAfter(source?.retryAfter, now);
754
+ if (parsed !== null) return parsed;
755
+ }
756
+ const headerBags = [err.headers, response?.headers, details?.headers, body?.headers];
757
+ for (const bag of headerBags) {
758
+ const parsed = parseRetryAfter(readHeader(bag, "retry-after"), now);
759
+ if (parsed !== null) return parsed;
760
+ }
761
+ for (const source of [details, bodyDetails, err]) {
762
+ for (const key of ["resetAt", "reset", "rateLimitReset"]) {
763
+ const parsed = parseResetAt(source?.[key], now);
764
+ if (parsed !== null) return parsed;
765
+ }
766
+ }
767
+ for (const bag of headerBags) {
768
+ for (const header of ["x-ratelimit-reset", "ratelimit-reset"]) {
769
+ const parsed = parseResetAt(readHeader(bag, header), now);
770
+ if (parsed !== null) return parsed;
771
+ }
772
+ }
773
+ return null;
774
+ }
775
+ function getHttpStatus(error) {
776
+ const err = asDict(error);
777
+ if (!err) return null;
778
+ for (const key of ["status", "statusCode", "httpStatus"]) {
779
+ const value = err[key];
780
+ if (typeof value === "number" && Number.isFinite(value)) return value;
781
+ if (typeof value === "string" && /^\d{3}$/.test(value)) return Number(value);
782
+ }
783
+ const response = asDict(err.response);
784
+ for (const key of ["status", "statusCode"]) {
785
+ const value = response?.[key];
786
+ if (typeof value === "number" && Number.isFinite(value)) return value;
787
+ }
788
+ return null;
789
+ }
790
+ function isTerminalClientError(error) {
791
+ const status = getHttpStatus(error);
792
+ if (status === null) return false;
793
+ return status >= 400 && status < 500 && status !== 429 && status !== 408;
794
+ }
795
+ var FAILURE_GROWTH_FACTOR = 2;
796
+ var IDLE_GROWTH_FACTOR = 1.5;
797
+ function nextFailureDelayMs(currentMs, maxMs) {
798
+ return Math.min(Math.max(currentMs, 1) * FAILURE_GROWTH_FACTOR, maxMs);
799
+ }
800
+ function nextIdleDelayMs(currentMs, maxMs) {
801
+ return Math.min(Math.ceil(Math.max(currentMs, 1) * IDLE_GROWTH_FACTOR), maxMs);
802
+ }
803
+ function applyJitter(delayMs, ratio, random = Math.random) {
804
+ if (ratio <= 0) return delayMs;
805
+ return Math.round(delayMs * (1 + random() * ratio));
806
+ }
807
+
669
808
  // src/streaming/stream-controller.ts
670
809
  var DEFAULT_STREAMING_CONFIG = {
671
810
  maxReconnectAttempts: 5,
672
811
  reconnectBaseDelayMs: 1e3,
673
812
  reconnectMaxDelayMs: 1e4,
813
+ // NOTE (unchanged, deliberately): this watchdog force-disconnects a HEALTHY
814
+ // SSE connection after 30s of silence and routes it through the same
815
+ // handleConnectionError path as a real transport failure. Ordinary LLM
816
+ // latency exceeds 30s routinely, so it fires against working streams — that
817
+ // is what drives sessions into the polling fallback in the first place. The
818
+ // fix is a server-side SSE heartbeat, not a bigger number here; raising this
819
+ // is a guess against a moving target. Everything below only bounds what the
820
+ // fallback costs once it has been entered.
674
821
  activityTimeoutMs: 3e4,
675
- pollingIntervalMs: 2e3
822
+ pollingIntervalMs: 5e3,
823
+ pollingMaxIntervalMs: 3e4,
824
+ pollingMaxConsecutiveFailures: 10,
825
+ pollingMaxConsecutiveRateLimits: 3,
826
+ pollingMaxConsecutiveClientErrors: 3,
827
+ pollingMaxDurationMs: 18e5,
828
+ pollingMaxRetryAfterMs: 3e5,
829
+ pollingJitterRatio: 0.1,
830
+ sseRetryIntervalMs: 6e4
676
831
  };
677
832
  var DEFAULT_POLLING_ENABLED = true;
678
833
  var ExecutionStreamControllerImpl = class {
@@ -685,9 +840,24 @@ var ExecutionStreamControllerImpl = class {
685
840
  seenEventIds = /* @__PURE__ */ new Set();
686
841
  _lastEventId = null;
687
842
  activityTimeoutId = null;
688
- pollingIntervalId = null;
689
843
  finalExecution = null;
690
844
  finalError = null;
845
+ // --- Fallback polling state ---
846
+ // A self-rescheduling timeout, not setInterval: the delay has to change
847
+ // between ticks (that is what backoff IS), and a timeout chain also cannot
848
+ // stack overlapping requests the way a fixed interval does when the API is
849
+ // slow to answer.
850
+ pollTimeoutId = null;
851
+ pollDelayMs = 0;
852
+ pollConsecutiveFailures = 0;
853
+ pollConsecutiveRateLimits = 0;
854
+ pollConsecutiveClientErrors = 0;
855
+ /** When the loop last observed anything new — the clock the give-up bound runs on. */
856
+ pollLastProgressAt = 0;
857
+ pollInFlight = false;
858
+ lastPolledStatus = null;
859
+ sseRetryTimeoutId = null;
860
+ sseRetryInFlight = false;
691
861
  // Event callbacks
692
862
  listeners = {
693
863
  log: [],
@@ -716,6 +886,39 @@ var ExecutionStreamControllerImpl = class {
716
886
  this.config = { ...DEFAULT_STREAMING_CONFIG, ...options.config };
717
887
  this.pollingEnabled = options.pollingEnabled ?? DEFAULT_POLLING_ENABLED;
718
888
  }
889
+ // Resolved fallback-poll settings. Read through `??` rather than off the
890
+ // spread config so an explicit `undefined` in a caller's partial config still
891
+ // lands on the default instead of NaN-ing the arithmetic.
892
+ get pollBaseIntervalMs() {
893
+ return this.config.pollingIntervalMs ?? DEFAULT_STREAMING_CONFIG.pollingIntervalMs;
894
+ }
895
+ get pollMaxIntervalMs() {
896
+ return Math.max(
897
+ this.config.pollingMaxIntervalMs ?? DEFAULT_STREAMING_CONFIG.pollingMaxIntervalMs,
898
+ this.pollBaseIntervalMs
899
+ );
900
+ }
901
+ get pollMaxConsecutiveFailures() {
902
+ return this.config.pollingMaxConsecutiveFailures ?? DEFAULT_STREAMING_CONFIG.pollingMaxConsecutiveFailures;
903
+ }
904
+ get pollMaxConsecutiveRateLimits() {
905
+ return this.config.pollingMaxConsecutiveRateLimits ?? DEFAULT_STREAMING_CONFIG.pollingMaxConsecutiveRateLimits;
906
+ }
907
+ get pollMaxConsecutiveClientErrors() {
908
+ return this.config.pollingMaxConsecutiveClientErrors ?? DEFAULT_STREAMING_CONFIG.pollingMaxConsecutiveClientErrors;
909
+ }
910
+ get pollMaxDurationMs() {
911
+ return this.config.pollingMaxDurationMs ?? DEFAULT_STREAMING_CONFIG.pollingMaxDurationMs;
912
+ }
913
+ get pollMaxRetryAfterMs() {
914
+ return this.config.pollingMaxRetryAfterMs ?? DEFAULT_STREAMING_CONFIG.pollingMaxRetryAfterMs;
915
+ }
916
+ get pollJitterRatio() {
917
+ return this.config.pollingJitterRatio ?? DEFAULT_STREAMING_CONFIG.pollingJitterRatio;
918
+ }
919
+ get sseRetryIntervalMs() {
920
+ return this.config.sseRetryIntervalMs ?? DEFAULT_STREAMING_CONFIG.sseRetryIntervalMs;
921
+ }
719
922
  /**
720
923
  * Get the last event ID (for resume support)
721
924
  */
@@ -806,28 +1009,45 @@ var ExecutionStreamControllerImpl = class {
806
1009
  console.log("[SDK:StreamController] executionId:", this.options.executionId);
807
1010
  console.log("[SDK:StreamController] orgId:", this.options.orgId);
808
1011
  console.log("[SDK:StreamController] apiKey:", authToken ? `${authToken.substring(0, 20)}...` : "MISSING");
809
- this.sseClient = new SSEClient({
1012
+ if (this.sseClient) {
1013
+ this.sseClient.disconnect();
1014
+ this.sseClient = null;
1015
+ }
1016
+ const client = new SSEClient({
810
1017
  url,
811
1018
  authHeader: `Bearer ${authToken}`,
812
1019
  orgId: this.options.orgId,
813
1020
  fetch: this.options.fetch,
814
- onEvent: (event) => this.handleEvent(event),
1021
+ // Resume from where this session left off. On a first connect this is
1022
+ // null and the header is omitted.
1023
+ lastEventId: this._lastEventId,
1024
+ onEvent: (event) => {
1025
+ if (this.sseClient !== client) return;
1026
+ this.handleEvent(event);
1027
+ },
815
1028
  onError: (error) => {
1029
+ if (this.sseClient !== client) return;
816
1030
  console.error("[SDK:StreamController] SSE error:", error);
817
1031
  this.handleConnectionError(error);
818
1032
  },
819
1033
  onConnectionChange: (connected) => {
1034
+ if (this.sseClient !== client) return;
820
1035
  console.log("[SDK:StreamController] SSE connection change:", connected);
821
1036
  if (connected) {
822
1037
  this.status = "connected";
823
1038
  this.reconnectAttempts = 0;
1039
+ this.stopPolling();
824
1040
  this.resetActivityTimeout();
825
1041
  }
826
- this.emit("connectionChange", { connected, usingFallback: false });
1042
+ this.emit("connectionChange", {
1043
+ connected,
1044
+ usingFallback: this.status === "fallback_polling"
1045
+ });
827
1046
  }
828
1047
  });
1048
+ this.sseClient = client;
829
1049
  try {
830
- await this.sseClient.connect();
1050
+ await client.connect();
831
1051
  console.log("[SDK:StreamController] SSE connect() completed");
832
1052
  } catch (error) {
833
1053
  console.error("[SDK:StreamController] SSE connect() threw:", error);
@@ -1066,6 +1286,7 @@ var ExecutionStreamControllerImpl = class {
1066
1286
  handleComplete(data) {
1067
1287
  this.status = "closed";
1068
1288
  this.stopActivityTimeout();
1289
+ this.stopPolling();
1069
1290
  const sessionId = data.sessionId ?? this.options.sessionId;
1070
1291
  const execution = {
1071
1292
  uuid: this.options.executionId,
@@ -1092,6 +1313,7 @@ var ExecutionStreamControllerImpl = class {
1092
1313
  handleError(error) {
1093
1314
  this.status = "closed";
1094
1315
  this.stopActivityTimeout();
1316
+ this.stopPolling();
1095
1317
  this.finalError = error;
1096
1318
  this.emit("error", error);
1097
1319
  if (this.waitReject) {
@@ -1118,6 +1340,10 @@ var ExecutionStreamControllerImpl = class {
1118
1340
  this.handleError({ error: msg, errorCode: "stream_unrecoverable" });
1119
1341
  return;
1120
1342
  }
1343
+ if (this.status === "fallback_polling") {
1344
+ this.startPolling();
1345
+ return;
1346
+ }
1121
1347
  if (this.reconnectAttempts < this.config.maxReconnectAttempts) {
1122
1348
  this.status = "reconnecting";
1123
1349
  this.reconnectAttempts++;
@@ -1187,41 +1413,215 @@ var ExecutionStreamControllerImpl = class {
1187
1413
  }
1188
1414
  }
1189
1415
  /**
1190
- * Start polling fallback
1416
+ * Start polling fallback.
1417
+ *
1418
+ * Idempotent — calling it while a loop is already running is a no-op, so no
1419
+ * path can end up with two loops racing the same execution.
1191
1420
  */
1192
1421
  startPolling() {
1193
- this.pollingIntervalId = setInterval(async () => {
1194
- try {
1195
- const execution = await this.options.getExecution(this.options.executionId);
1196
- for (const log of execution.logs) {
1197
- if (!this.seenEventIds.has(log.uuid)) {
1198
- this.seenEventIds.add(log.uuid);
1199
- this.emit("log", log);
1200
- }
1201
- }
1202
- if (execution.status === "completed") {
1203
- this.stopPolling();
1204
- this.handleComplete(execution);
1205
- } else if (execution.status === "failed" || execution.status === "cancelled") {
1206
- this.stopPolling();
1207
- this.handleError({
1208
- error: execution.error ?? `Execution ${execution.status}`,
1209
- errorCode: execution.status
1210
- });
1422
+ if (this.pollTimeoutId !== null || this.pollInFlight) {
1423
+ return;
1424
+ }
1425
+ this.pollLastProgressAt = Date.now();
1426
+ this.pollDelayMs = this.pollBaseIntervalMs;
1427
+ this.pollConsecutiveFailures = 0;
1428
+ this.pollConsecutiveRateLimits = 0;
1429
+ this.pollConsecutiveClientErrors = 0;
1430
+ this.lastPolledStatus = null;
1431
+ this.stopActivityTimeout();
1432
+ this.scheduleSSERetry();
1433
+ this.schedulePoll(this.pollDelayMs);
1434
+ }
1435
+ /**
1436
+ * Queue the next poll tick.
1437
+ */
1438
+ schedulePoll(delayMs) {
1439
+ if (this.status !== "fallback_polling") {
1440
+ return;
1441
+ }
1442
+ this.pollTimeoutId = setTimeout(() => {
1443
+ this.pollTimeoutId = null;
1444
+ void this.runPoll();
1445
+ }, Math.max(0, Math.round(delayMs)));
1446
+ }
1447
+ /**
1448
+ * One poll tick: fetch state, emit what is new, then decide how long to wait
1449
+ * before the next one — or whether there should be a next one at all.
1450
+ */
1451
+ async runPoll() {
1452
+ if (this.status !== "fallback_polling") {
1453
+ return;
1454
+ }
1455
+ if (Date.now() - this.pollLastProgressAt >= this.pollMaxDurationMs) {
1456
+ this.failPolling(
1457
+ `Streaming fallback gave up: no execution progress for ${Math.round(this.pollMaxDurationMs / 1e3)}s`,
1458
+ "polling_timeout"
1459
+ );
1460
+ return;
1461
+ }
1462
+ this.pollInFlight = true;
1463
+ try {
1464
+ const execution = await this.options.getExecution(this.options.executionId);
1465
+ this.pollConsecutiveFailures = 0;
1466
+ this.pollConsecutiveRateLimits = 0;
1467
+ this.pollConsecutiveClientErrors = 0;
1468
+ let sawNewInfo = false;
1469
+ for (const log of execution.logs ?? []) {
1470
+ if (!this.seenEventIds.has(log.uuid)) {
1471
+ this.seenEventIds.add(log.uuid);
1472
+ this.emit("log", log);
1473
+ sawNewInfo = true;
1211
1474
  }
1212
- } catch (error) {
1213
- console.error("Polling error:", error);
1214
1475
  }
1215
- }, this.config.pollingIntervalMs);
1476
+ if (execution.status !== this.lastPolledStatus) {
1477
+ this.lastPolledStatus = execution.status;
1478
+ sawNewInfo = true;
1479
+ }
1480
+ if (sawNewInfo) {
1481
+ this.pollLastProgressAt = Date.now();
1482
+ }
1483
+ if (execution.status === "completed") {
1484
+ this.stopPolling();
1485
+ this.handleComplete(execution);
1486
+ return;
1487
+ }
1488
+ if (execution.status === "failed" || execution.status === "cancelled") {
1489
+ this.stopPolling();
1490
+ this.handleError({
1491
+ error: execution.error ?? `Execution ${execution.status}`,
1492
+ errorCode: execution.status
1493
+ });
1494
+ return;
1495
+ }
1496
+ this.pollDelayMs = sawNewInfo ? this.pollBaseIntervalMs : nextIdleDelayMs(this.pollDelayMs, this.pollMaxIntervalMs);
1497
+ } catch (error) {
1498
+ if (!this.handlePollError(error)) {
1499
+ return;
1500
+ }
1501
+ } finally {
1502
+ this.pollInFlight = false;
1503
+ }
1504
+ this.schedulePoll(applyJitter(this.pollDelayMs, this.pollJitterRatio));
1505
+ }
1506
+ /**
1507
+ * Apply backpressure for a failed poll.
1508
+ *
1509
+ * @returns `true` to keep polling (with `pollDelayMs` updated), `false` when
1510
+ * the loop has been terminated.
1511
+ */
1512
+ handlePollError(error) {
1513
+ const retryAfterMs = getRetryAfterMs(error);
1514
+ const rateLimited = isRateLimited(error) || retryAfterMs !== null;
1515
+ if (rateLimited) {
1516
+ this.pollConsecutiveRateLimits++;
1517
+ console.warn(
1518
+ "[SDK:StreamController] Poll rate-limited",
1519
+ retryAfterMs !== null ? `(server asked for ${Math.round(retryAfterMs / 1e3)}s)` : "(no reset named)"
1520
+ );
1521
+ if (retryAfterMs !== null && retryAfterMs > this.pollMaxRetryAfterMs) {
1522
+ this.failPolling(
1523
+ `Rate limited: the API asked for ${Math.round(retryAfterMs / 1e3)}s, longer than this stream will wait`,
1524
+ "rate_limited"
1525
+ );
1526
+ return false;
1527
+ }
1528
+ if (this.pollConsecutiveRateLimits >= this.pollMaxConsecutiveRateLimits) {
1529
+ this.failPolling(
1530
+ "Rate limited by the API; stopped the streaming fallback rather than keep spending the key quota",
1531
+ "rate_limited"
1532
+ );
1533
+ return false;
1534
+ }
1535
+ this.pollDelayMs = Math.max(retryAfterMs ?? UNNAMED_RATE_LIMIT_WAIT_MS, this.pollDelayMs);
1536
+ return true;
1537
+ }
1538
+ if (isTerminalClientError(error)) {
1539
+ this.pollConsecutiveClientErrors++;
1540
+ const status = getHttpStatus(error);
1541
+ console.warn(
1542
+ `[SDK:StreamController] Poll got HTTP ${status} (${this.pollConsecutiveClientErrors}/${this.pollMaxConsecutiveClientErrors})`
1543
+ );
1544
+ if (this.pollConsecutiveClientErrors >= this.pollMaxConsecutiveClientErrors) {
1545
+ this.failPolling(
1546
+ `Execution status endpoint returned HTTP ${status} ${this.pollConsecutiveClientErrors} times \u2014 cannot track this run`,
1547
+ "polling_unrecoverable"
1548
+ );
1549
+ return false;
1550
+ }
1551
+ this.pollDelayMs = nextFailureDelayMs(this.pollDelayMs, this.pollMaxIntervalMs);
1552
+ return true;
1553
+ }
1554
+ this.pollConsecutiveFailures++;
1555
+ console.warn(
1556
+ `[SDK:StreamController] Poll failed (${this.pollConsecutiveFailures}/${this.pollMaxConsecutiveFailures}):`,
1557
+ error
1558
+ );
1559
+ if (this.pollConsecutiveFailures >= this.pollMaxConsecutiveFailures) {
1560
+ this.failPolling(
1561
+ `Streaming fallback failed ${this.pollConsecutiveFailures} times in a row`,
1562
+ "polling_failed"
1563
+ );
1564
+ return false;
1565
+ }
1566
+ this.pollDelayMs = nextFailureDelayMs(this.pollDelayMs, this.pollMaxIntervalMs);
1567
+ return true;
1216
1568
  }
1217
1569
  /**
1218
- * Stop polling
1570
+ * Terminate the fallback loop and surface the reason.
1571
+ */
1572
+ failPolling(error, errorCode) {
1573
+ this.stopPolling();
1574
+ this.handleError({ error, errorCode });
1575
+ }
1576
+ /**
1577
+ * Stop polling, including the periodic SSE re-attempt that belongs to it.
1219
1578
  */
1220
1579
  stopPolling() {
1221
- if (this.pollingIntervalId) {
1222
- clearInterval(this.pollingIntervalId);
1223
- this.pollingIntervalId = null;
1580
+ if (this.pollTimeoutId !== null) {
1581
+ clearTimeout(this.pollTimeoutId);
1582
+ this.pollTimeoutId = null;
1583
+ }
1584
+ if (this.sseRetryTimeoutId !== null) {
1585
+ clearTimeout(this.sseRetryTimeoutId);
1586
+ this.sseRetryTimeoutId = null;
1587
+ }
1588
+ }
1589
+ /**
1590
+ * Queue the next attempt to get the real stream back.
1591
+ */
1592
+ scheduleSSERetry() {
1593
+ if (this.sseRetryTimeoutId !== null || this.status !== "fallback_polling") {
1594
+ return;
1595
+ }
1596
+ this.sseRetryTimeoutId = setTimeout(() => {
1597
+ this.sseRetryTimeoutId = null;
1598
+ this.attemptSSERecovery();
1599
+ }, this.sseRetryIntervalMs);
1600
+ }
1601
+ /**
1602
+ * Re-attempt SSE from inside the fallback, so a transient outage costs the
1603
+ * session a few polls rather than its stream for the lifetime of the tab.
1604
+ *
1605
+ * Success is NOT observed here: `SSEClient.connect()` resolves when the
1606
+ * stream ENDS, not when it opens. The open is observed by
1607
+ * `onConnectionChange(true)`, which flips status and stops this loop. The
1608
+ * promise below only tells us the attempt is over so the next one can be
1609
+ * queued.
1610
+ */
1611
+ attemptSSERecovery() {
1612
+ if (this.status !== "fallback_polling") {
1613
+ return;
1224
1614
  }
1615
+ if (this.sseRetryInFlight) {
1616
+ this.scheduleSSERetry();
1617
+ return;
1618
+ }
1619
+ this.sseRetryInFlight = true;
1620
+ void this.connectSSE().catch(() => {
1621
+ }).finally(() => {
1622
+ this.sseRetryInFlight = false;
1623
+ this.scheduleSSERetry();
1624
+ });
1225
1625
  }
1226
1626
  /**
1227
1627
  * Reset activity timeout
@@ -1353,12 +1753,23 @@ async function collectImagesFromStream(stream, options = {}) {
1353
1753
  });
1354
1754
  }
1355
1755
 
1756
+ // src/utils/api-paths.ts
1757
+ var API_V1 = "/api/v1";
1758
+ var EXECUTION_PATH = `${API_V1}/executions`;
1759
+ function executionPath(executionId) {
1760
+ return `${EXECUTION_PATH}/${executionId}`;
1761
+ }
1762
+ function executionCancelPath(executionId) {
1763
+ return `${EXECUTION_PATH}/${executionId}/cancel`;
1764
+ }
1765
+
1356
1766
  // src/resources/agents.ts
1357
1767
  var AgentsAPI = class extends BaseResourceAPI {
1358
1768
  constructor(http, getStreamConfig) {
1359
1769
  super(http, "/api/v1/agents");
1360
1770
  this.getStreamConfig = getStreamConfig;
1361
1771
  }
1772
+ getStreamConfig;
1362
1773
  // ===== LIST Operations =====
1363
1774
  /**
1364
1775
  * List all agents in the organization
@@ -1615,7 +2026,7 @@ var AgentsAPI = class extends BaseResourceAPI {
1615
2026
  * ```
1616
2027
  */
1617
2028
  async getExecution(executionId) {
1618
- return this.http.get(`/executions/${executionId}`);
2029
+ return this.http.get(executionPath(executionId));
1619
2030
  }
1620
2031
  // ===== EDIT Operations =====
1621
2032
  /**
@@ -1670,7 +2081,7 @@ var AgentsAPI = class extends BaseResourceAPI {
1670
2081
  */
1671
2082
  async cancelExecution(executionId) {
1672
2083
  return this.http.post(
1673
- `/executions/${executionId}/cancel`,
2084
+ executionCancelPath(executionId),
1674
2085
  void 0,
1675
2086
  { skipRetry: true }
1676
2087
  );
@@ -2171,6 +2582,7 @@ var ArtifactsAPI = class extends BaseResourceAPI {
2171
2582
  }
2172
2583
  return this.http.request({
2173
2584
  method: "POST",
2585
+ // KNOWN BROKEN — same missing `/api/v1` prefix as listArtifacts above.
2174
2586
  path: `/executions/${executionId}/artifacts`,
2175
2587
  body: formData,
2176
2588
  headers: {
@@ -2264,6 +2676,7 @@ var SessionsAPI = class extends BaseResourceAPI {
2264
2676
  super(http, "/api/v1/sessions");
2265
2677
  this.getStreamConfig = getStreamConfig;
2266
2678
  }
2679
+ getStreamConfig;
2267
2680
  // ===== LIST Operations =====
2268
2681
  /**
2269
2682
  * List all sessions in the organization
@@ -2443,7 +2856,7 @@ var SessionsAPI = class extends BaseResourceAPI {
2443
2856
  * Get execution result
2444
2857
  */
2445
2858
  async getExecution(executionId) {
2446
- return this.http.get(`/executions/${executionId}`);
2859
+ return this.http.get(executionPath(executionId));
2447
2860
  }
2448
2861
  // ===== EDIT Operations =====
2449
2862
  /**
@@ -2583,6 +2996,7 @@ var KanbanAPI = class extends BaseResourceAPI {
2583
2996
  super(http, "/api/v1/kanban");
2584
2997
  this.getStreamConfig = getStreamConfig;
2585
2998
  }
2999
+ getStreamConfig;
2586
3000
  // ===== BOARD =====
2587
3001
  /**
2588
3002
  * Get the current org's board (lanes in declared order). Auto-provisions
@@ -2717,7 +3131,7 @@ var KanbanAPI = class extends BaseResourceAPI {
2717
3131
  * @returns Execution details.
2718
3132
  */
2719
3133
  async getCardRun(executionId) {
2720
- return this.http.get(`/api/v1/agents/executions/${executionId}`);
3134
+ return this.http.get(executionPath(executionId));
2721
3135
  }
2722
3136
  /**
2723
3137
  * Create a stream controller for a card's run, keyed on the card's
@@ -2784,6 +3198,7 @@ var RemoteAgentsAPI = class extends BaseResourceAPI {
2784
3198
  super(http, "/api/v1/remote-agents");
2785
3199
  this.getStreamConfig = getStreamConfig;
2786
3200
  }
3201
+ getStreamConfig;
2787
3202
  /**
2788
3203
  * List all remote agents in the organization
2789
3204
  *
@@ -2917,7 +3332,7 @@ var RemoteAgentsAPI = class extends BaseResourceAPI {
2917
3332
  apiKey: config.apiKey,
2918
3333
  tokenProvider: config.tokenProvider,
2919
3334
  orgId: config.orgId,
2920
- getExecution: (id) => this.http.get(`/executions/${id}`)
3335
+ getExecution: (id) => this.http.get(executionPath(id))
2921
3336
  });
2922
3337
  return controller;
2923
3338
  }
@@ -3998,6 +4413,13 @@ function initialState() {
3998
4413
 
3999
4414
  // src/chat/hooks/useAgentChat.ts
4000
4415
  var __DEV__ = typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
4416
+ var LEGACY_POLL_BASE_INTERVAL_MS = 5e3;
4417
+ var LEGACY_POLL_MAX_INTERVAL_MS = 3e4;
4418
+ var LEGACY_POLL_MAX_FAILURES = 10;
4419
+ var LEGACY_POLL_MAX_RATE_LIMITS = 3;
4420
+ var LEGACY_POLL_MAX_IDLE_MS = 18e5;
4421
+ var LEGACY_POLL_MAX_RETRY_AFTER_MS = 3e5;
4422
+ var LEGACY_POLL_JITTER_RATIO = 0.1;
4001
4423
  var SILENT_TOOL_NAMES = /* @__PURE__ */ new Set([
4002
4424
  "check_delegations"
4003
4425
  ]);
@@ -4585,12 +5007,17 @@ function useAgentChat(options) {
4585
5007
  }
4586
5008
  }
4587
5009
  });
4588
- const pollingIntervalRef = useRef(null);
5010
+ const pollingTimeoutRef = useRef(null);
4589
5011
  const pollingExecutionIdRef = useRef(null);
5012
+ const pollDelayRef = useRef(0);
5013
+ const pollFailuresRef = useRef(0);
5014
+ const pollRateLimitsRef = useRef(0);
5015
+ const pollLastProgressRef = useRef(0);
5016
+ const pollLastStatusRef = useRef(null);
4590
5017
  const stopPolling = useCallback(() => {
4591
- if (pollingIntervalRef.current) {
4592
- clearInterval(pollingIntervalRef.current);
4593
- pollingIntervalRef.current = null;
5018
+ if (pollingTimeoutRef.current) {
5019
+ clearTimeout(pollingTimeoutRef.current);
5020
+ pollingTimeoutRef.current = null;
4594
5021
  }
4595
5022
  pollingExecutionIdRef.current = null;
4596
5023
  }, []);
@@ -4598,26 +5025,77 @@ function useAgentChat(options) {
4598
5025
  if (pollingExecutionIdRef.current === executionId) return;
4599
5026
  stopPolling();
4600
5027
  pollingExecutionIdRef.current = executionId;
5028
+ const baseDelayMs = pollingInterval ?? LEGACY_POLL_BASE_INTERVAL_MS;
5029
+ pollDelayRef.current = baseDelayMs;
5030
+ pollFailuresRef.current = 0;
5031
+ pollRateLimitsRef.current = 0;
5032
+ pollLastProgressRef.current = Date.now();
5033
+ pollLastStatusRef.current = null;
4601
5034
  if (__DEV__) console.log("[useAgentChat] Starting polling fallback for:", executionId);
5035
+ const schedule = (delayMs) => {
5036
+ if (pollingExecutionIdRef.current !== executionId) return;
5037
+ pollingTimeoutRef.current = setTimeout(() => {
5038
+ pollingTimeoutRef.current = null;
5039
+ void poll();
5040
+ }, Math.max(0, Math.round(delayMs)));
5041
+ };
5042
+ const giveUp = (message) => {
5043
+ if (__DEV__) console.warn("[useAgentChat] Polling gave up:", message);
5044
+ stopPolling();
5045
+ handleError(message);
5046
+ };
4602
5047
  const poll = async () => {
4603
5048
  if (pollingExecutionIdRef.current !== executionId) return;
5049
+ if (Date.now() - pollLastProgressRef.current >= LEGACY_POLL_MAX_IDLE_MS) {
5050
+ giveUp("Lost the live connection and stopped receiving updates for this run");
5051
+ return;
5052
+ }
4604
5053
  try {
4605
5054
  const execution = await adapter.getExecution(executionId);
5055
+ pollFailuresRef.current = 0;
5056
+ pollRateLimitsRef.current = 0;
5057
+ if (execution.status !== pollLastStatusRef.current) {
5058
+ pollLastStatusRef.current = execution.status;
5059
+ pollLastProgressRef.current = Date.now();
5060
+ pollDelayRef.current = baseDelayMs;
5061
+ } else {
5062
+ pollDelayRef.current = nextIdleDelayMs(pollDelayRef.current, LEGACY_POLL_MAX_INTERVAL_MS);
5063
+ }
4606
5064
  if (execution.status === "completed") {
4607
5065
  if (__DEV__) console.log("[useAgentChat] Polling: execution completed");
4608
5066
  stopPolling();
4609
5067
  handleComplete(execution.result || void 0);
4610
- } else if (execution.status === "failed") {
5068
+ return;
5069
+ }
5070
+ if (execution.status === "failed") {
4611
5071
  if (__DEV__) console.log("[useAgentChat] Polling: execution failed");
4612
5072
  stopPolling();
4613
5073
  handleError(execution.error || "Execution failed");
5074
+ return;
4614
5075
  }
4615
5076
  } catch (err) {
4616
- if (__DEV__) console.warn("[useAgentChat] Polling error (will retry):", err);
5077
+ const retryAfterMs = getRetryAfterMs(err);
5078
+ if (isRateLimited(err) || retryAfterMs !== null) {
5079
+ pollRateLimitsRef.current++;
5080
+ const resetTooFarOut = retryAfterMs !== null && retryAfterMs > LEGACY_POLL_MAX_RETRY_AFTER_MS;
5081
+ if (resetTooFarOut || pollRateLimitsRef.current >= LEGACY_POLL_MAX_RATE_LIMITS) {
5082
+ giveUp("Rate limited by the API \u2014 stopped polling for updates on this run");
5083
+ return;
5084
+ }
5085
+ pollDelayRef.current = Math.max(retryAfterMs ?? UNNAMED_RATE_LIMIT_WAIT_MS, pollDelayRef.current);
5086
+ } else {
5087
+ pollFailuresRef.current++;
5088
+ if (__DEV__) console.warn("[useAgentChat] Polling error (will retry):", err);
5089
+ if (pollFailuresRef.current >= LEGACY_POLL_MAX_FAILURES) {
5090
+ giveUp("Lost connection to the agent \u2014 stopped polling for updates on this run");
5091
+ return;
5092
+ }
5093
+ pollDelayRef.current = nextFailureDelayMs(pollDelayRef.current, LEGACY_POLL_MAX_INTERVAL_MS);
5094
+ }
4617
5095
  }
5096
+ schedule(applyJitter(pollDelayRef.current, LEGACY_POLL_JITTER_RATIO));
4618
5097
  };
4619
- poll();
4620
- pollingIntervalRef.current = setInterval(poll, pollingInterval ?? 2e3);
5098
+ void poll();
4621
5099
  }, [adapter, stopPolling, handleComplete, handleError, pollingInterval]);
4622
5100
  useEffect(() => {
4623
5101
  if (!useStreamController && isUsingFallback && currentExecutionId && isLoading && pollingEnabled !== false) {
@@ -8587,7 +9065,8 @@ function AgentChat({
8587
9065
  hideHeaderWhileEmpty = false,
8588
9066
  hideTypingIndicator = false,
8589
9067
  onInputChange,
8590
- onInputControlReady
9068
+ onInputControlReady,
9069
+ injectedMessages
8591
9070
  }) {
8592
9071
  const {
8593
9072
  messages,
@@ -8651,8 +9130,9 @@ function AgentChat({
8651
9130
  },
8652
9131
  [enableAttachments, attachments, sendMessage]
8653
9132
  );
9133
+ const renderedMessages = injectedMessages && injectedMessages.length > 0 ? [...messages, ...injectedMessages].filter((m, i, arr) => arr.findIndex((x) => x.id === m.id) === i).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()) : messages;
8654
9134
  const messageListProps = {
8655
- messages,
9135
+ messages: renderedMessages,
8656
9136
  welcomeMessage: hideHeader ? void 0 : welcomeMessage,
8657
9137
  renderWelcome: hideHeader ? void 0 : renderWelcome,
8658
9138
  isLoading,