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