@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.
package/dist/index.js CHANGED
@@ -433,7 +433,8 @@ var SSEClient = class {
433
433
  "Authorization": this.config.authHeader,
434
434
  "X-Org-Id": String(this.config.orgId),
435
435
  "Accept": "text/event-stream",
436
- "Cache-Control": "no-cache"
436
+ "Cache-Control": "no-cache",
437
+ ...this.config.lastEventId ? { "Last-Event-ID": this.config.lastEventId } : {}
437
438
  },
438
439
  signal: this.abortController.signal
439
440
  });
@@ -659,13 +660,167 @@ function normalizeUrl(url) {
659
660
  return url;
660
661
  }
661
662
 
663
+ // src/streaming/poll-policy.ts
664
+ var UNNAMED_RATE_LIMIT_WAIT_MS = 6e4;
665
+ function asDict(value) {
666
+ return typeof value === "object" && value !== null ? value : null;
667
+ }
668
+ var RATE_LIMIT_CODES = /* @__PURE__ */ new Set([
669
+ "rate_limit_error",
670
+ "rate_limited",
671
+ "rate_limit_exceeded",
672
+ "too_many_requests"
673
+ ]);
674
+ function isRateLimited(error) {
675
+ if (typeof error === "string") return /\b429\b/.test(error);
676
+ const err = asDict(error);
677
+ if (!err) return false;
678
+ for (const key of ["status", "statusCode", "httpStatus", "code"]) {
679
+ if (err[key] === 429 || err[key] === "429") return true;
680
+ }
681
+ const response = asDict(err.response);
682
+ if (response && (response.status === 429 || response.statusCode === 429)) return true;
683
+ if (typeof err.code === "string" && RATE_LIMIT_CODES.has(err.code.toLowerCase())) return true;
684
+ if (err.name === "RateLimitError") return true;
685
+ const message = typeof err.message === "string" ? err.message : "";
686
+ return /\b429\b/.test(message) || /rate[\s_-]?limit/i.test(message) || /too many requests/i.test(message);
687
+ }
688
+ function readHeader(bag, name) {
689
+ if (bag === null || bag === void 0) return null;
690
+ const getter = bag.get;
691
+ if (typeof getter === "function") {
692
+ try {
693
+ const value = getter.call(bag, name);
694
+ if (value !== null && value !== void 0) return String(value);
695
+ } catch {
696
+ }
697
+ }
698
+ const dict = asDict(bag);
699
+ if (!dict) return null;
700
+ const wanted = name.toLowerCase();
701
+ for (const [key, value] of Object.entries(dict)) {
702
+ if (key.toLowerCase() === wanted && value !== null && value !== void 0) {
703
+ return String(value);
704
+ }
705
+ }
706
+ return null;
707
+ }
708
+ function parseRetryAfter(raw, now) {
709
+ if (raw === null || raw === void 0) return null;
710
+ if (typeof raw === "number") {
711
+ return Number.isFinite(raw) && raw >= 0 ? raw * 1e3 : null;
712
+ }
713
+ if (typeof raw !== "string") return null;
714
+ const trimmed = raw.trim();
715
+ if (trimmed === "") return null;
716
+ if (/^\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed) * 1e3;
717
+ const at = Date.parse(trimmed);
718
+ return Number.isNaN(at) ? null : Math.max(0, at - now);
719
+ }
720
+ function parseResetAt(raw, now) {
721
+ if (raw === null || raw === void 0) return null;
722
+ const numeric = typeof raw === "number" ? raw : typeof raw === "string" && /^\d+(\.\d+)?$/.test(raw.trim()) ? Number(raw.trim()) : null;
723
+ if (numeric !== null) {
724
+ if (!Number.isFinite(numeric) || numeric < 0) return null;
725
+ if (numeric > 1e11) return Math.max(0, numeric - now);
726
+ if (numeric > 1e9) return Math.max(0, numeric * 1e3 - now);
727
+ return numeric * 1e3;
728
+ }
729
+ if (typeof raw !== "string") return null;
730
+ const at = Date.parse(raw.trim());
731
+ return Number.isNaN(at) ? null : Math.max(0, at - now);
732
+ }
733
+ function getRetryAfterMs(error, now = Date.now()) {
734
+ const err = asDict(error);
735
+ if (!err) return null;
736
+ const details = asDict(err.details);
737
+ const response = asDict(err.response);
738
+ const body = asDict(err.body) ?? asDict(response?.data);
739
+ const bodyError = asDict(body?.error);
740
+ const bodyDetails = asDict(bodyError?.details) ?? asDict(body?.details);
741
+ for (const source of [err, details, bodyDetails]) {
742
+ const ms = source?.retryAfterMs;
743
+ if (typeof ms === "number" && Number.isFinite(ms) && ms >= 0) return ms;
744
+ }
745
+ for (const source of [err, details, bodyDetails, bodyError]) {
746
+ const parsed = parseRetryAfter(source?.retryAfter, now);
747
+ if (parsed !== null) return parsed;
748
+ }
749
+ const headerBags = [err.headers, response?.headers, details?.headers, body?.headers];
750
+ for (const bag of headerBags) {
751
+ const parsed = parseRetryAfter(readHeader(bag, "retry-after"), now);
752
+ if (parsed !== null) return parsed;
753
+ }
754
+ for (const source of [details, bodyDetails, err]) {
755
+ for (const key of ["resetAt", "reset", "rateLimitReset"]) {
756
+ const parsed = parseResetAt(source?.[key], now);
757
+ if (parsed !== null) return parsed;
758
+ }
759
+ }
760
+ for (const bag of headerBags) {
761
+ for (const header of ["x-ratelimit-reset", "ratelimit-reset"]) {
762
+ const parsed = parseResetAt(readHeader(bag, header), now);
763
+ if (parsed !== null) return parsed;
764
+ }
765
+ }
766
+ return null;
767
+ }
768
+ function getHttpStatus(error) {
769
+ const err = asDict(error);
770
+ if (!err) return null;
771
+ for (const key of ["status", "statusCode", "httpStatus"]) {
772
+ const value = err[key];
773
+ if (typeof value === "number" && Number.isFinite(value)) return value;
774
+ if (typeof value === "string" && /^\d{3}$/.test(value)) return Number(value);
775
+ }
776
+ const response = asDict(err.response);
777
+ for (const key of ["status", "statusCode"]) {
778
+ const value = response?.[key];
779
+ if (typeof value === "number" && Number.isFinite(value)) return value;
780
+ }
781
+ return null;
782
+ }
783
+ function isTerminalClientError(error) {
784
+ const status = getHttpStatus(error);
785
+ if (status === null) return false;
786
+ return status >= 400 && status < 500 && status !== 429 && status !== 408;
787
+ }
788
+ var FAILURE_GROWTH_FACTOR = 2;
789
+ var IDLE_GROWTH_FACTOR = 1.5;
790
+ function nextFailureDelayMs(currentMs, maxMs) {
791
+ return Math.min(Math.max(currentMs, 1) * FAILURE_GROWTH_FACTOR, maxMs);
792
+ }
793
+ function nextIdleDelayMs(currentMs, maxMs) {
794
+ return Math.min(Math.ceil(Math.max(currentMs, 1) * IDLE_GROWTH_FACTOR), maxMs);
795
+ }
796
+ function applyJitter(delayMs, ratio, random = Math.random) {
797
+ if (ratio <= 0) return delayMs;
798
+ return Math.round(delayMs * (1 + random() * ratio));
799
+ }
800
+
662
801
  // src/streaming/stream-controller.ts
663
802
  var DEFAULT_STREAMING_CONFIG = {
664
803
  maxReconnectAttempts: 5,
665
804
  reconnectBaseDelayMs: 1e3,
666
805
  reconnectMaxDelayMs: 1e4,
806
+ // NOTE (unchanged, deliberately): this watchdog force-disconnects a HEALTHY
807
+ // SSE connection after 30s of silence and routes it through the same
808
+ // handleConnectionError path as a real transport failure. Ordinary LLM
809
+ // latency exceeds 30s routinely, so it fires against working streams — that
810
+ // is what drives sessions into the polling fallback in the first place. The
811
+ // fix is a server-side SSE heartbeat, not a bigger number here; raising this
812
+ // is a guess against a moving target. Everything below only bounds what the
813
+ // fallback costs once it has been entered.
667
814
  activityTimeoutMs: 3e4,
668
- pollingIntervalMs: 2e3
815
+ pollingIntervalMs: 5e3,
816
+ pollingMaxIntervalMs: 3e4,
817
+ pollingMaxConsecutiveFailures: 10,
818
+ pollingMaxConsecutiveRateLimits: 3,
819
+ pollingMaxConsecutiveClientErrors: 3,
820
+ pollingMaxDurationMs: 18e5,
821
+ pollingMaxRetryAfterMs: 3e5,
822
+ pollingJitterRatio: 0.1,
823
+ sseRetryIntervalMs: 6e4
669
824
  };
670
825
  var DEFAULT_POLLING_ENABLED = true;
671
826
  var ExecutionStreamControllerImpl = class {
@@ -678,9 +833,24 @@ var ExecutionStreamControllerImpl = class {
678
833
  seenEventIds = /* @__PURE__ */ new Set();
679
834
  _lastEventId = null;
680
835
  activityTimeoutId = null;
681
- pollingIntervalId = null;
682
836
  finalExecution = null;
683
837
  finalError = null;
838
+ // --- Fallback polling state ---
839
+ // A self-rescheduling timeout, not setInterval: the delay has to change
840
+ // between ticks (that is what backoff IS), and a timeout chain also cannot
841
+ // stack overlapping requests the way a fixed interval does when the API is
842
+ // slow to answer.
843
+ pollTimeoutId = null;
844
+ pollDelayMs = 0;
845
+ pollConsecutiveFailures = 0;
846
+ pollConsecutiveRateLimits = 0;
847
+ pollConsecutiveClientErrors = 0;
848
+ /** When the loop last observed anything new — the clock the give-up bound runs on. */
849
+ pollLastProgressAt = 0;
850
+ pollInFlight = false;
851
+ lastPolledStatus = null;
852
+ sseRetryTimeoutId = null;
853
+ sseRetryInFlight = false;
684
854
  // Event callbacks
685
855
  listeners = {
686
856
  log: [],
@@ -709,6 +879,39 @@ var ExecutionStreamControllerImpl = class {
709
879
  this.config = { ...DEFAULT_STREAMING_CONFIG, ...options.config };
710
880
  this.pollingEnabled = options.pollingEnabled ?? DEFAULT_POLLING_ENABLED;
711
881
  }
882
+ // Resolved fallback-poll settings. Read through `??` rather than off the
883
+ // spread config so an explicit `undefined` in a caller's partial config still
884
+ // lands on the default instead of NaN-ing the arithmetic.
885
+ get pollBaseIntervalMs() {
886
+ return this.config.pollingIntervalMs ?? DEFAULT_STREAMING_CONFIG.pollingIntervalMs;
887
+ }
888
+ get pollMaxIntervalMs() {
889
+ return Math.max(
890
+ this.config.pollingMaxIntervalMs ?? DEFAULT_STREAMING_CONFIG.pollingMaxIntervalMs,
891
+ this.pollBaseIntervalMs
892
+ );
893
+ }
894
+ get pollMaxConsecutiveFailures() {
895
+ return this.config.pollingMaxConsecutiveFailures ?? DEFAULT_STREAMING_CONFIG.pollingMaxConsecutiveFailures;
896
+ }
897
+ get pollMaxConsecutiveRateLimits() {
898
+ return this.config.pollingMaxConsecutiveRateLimits ?? DEFAULT_STREAMING_CONFIG.pollingMaxConsecutiveRateLimits;
899
+ }
900
+ get pollMaxConsecutiveClientErrors() {
901
+ return this.config.pollingMaxConsecutiveClientErrors ?? DEFAULT_STREAMING_CONFIG.pollingMaxConsecutiveClientErrors;
902
+ }
903
+ get pollMaxDurationMs() {
904
+ return this.config.pollingMaxDurationMs ?? DEFAULT_STREAMING_CONFIG.pollingMaxDurationMs;
905
+ }
906
+ get pollMaxRetryAfterMs() {
907
+ return this.config.pollingMaxRetryAfterMs ?? DEFAULT_STREAMING_CONFIG.pollingMaxRetryAfterMs;
908
+ }
909
+ get pollJitterRatio() {
910
+ return this.config.pollingJitterRatio ?? DEFAULT_STREAMING_CONFIG.pollingJitterRatio;
911
+ }
912
+ get sseRetryIntervalMs() {
913
+ return this.config.sseRetryIntervalMs ?? DEFAULT_STREAMING_CONFIG.sseRetryIntervalMs;
914
+ }
712
915
  /**
713
916
  * Get the last event ID (for resume support)
714
917
  */
@@ -799,28 +1002,45 @@ var ExecutionStreamControllerImpl = class {
799
1002
  console.log("[SDK:StreamController] executionId:", this.options.executionId);
800
1003
  console.log("[SDK:StreamController] orgId:", this.options.orgId);
801
1004
  console.log("[SDK:StreamController] apiKey:", authToken ? `${authToken.substring(0, 20)}...` : "MISSING");
802
- this.sseClient = new SSEClient({
1005
+ if (this.sseClient) {
1006
+ this.sseClient.disconnect();
1007
+ this.sseClient = null;
1008
+ }
1009
+ const client = new SSEClient({
803
1010
  url,
804
1011
  authHeader: `Bearer ${authToken}`,
805
1012
  orgId: this.options.orgId,
806
1013
  fetch: this.options.fetch,
807
- onEvent: (event) => this.handleEvent(event),
1014
+ // Resume from where this session left off. On a first connect this is
1015
+ // null and the header is omitted.
1016
+ lastEventId: this._lastEventId,
1017
+ onEvent: (event) => {
1018
+ if (this.sseClient !== client) return;
1019
+ this.handleEvent(event);
1020
+ },
808
1021
  onError: (error) => {
1022
+ if (this.sseClient !== client) return;
809
1023
  console.error("[SDK:StreamController] SSE error:", error);
810
1024
  this.handleConnectionError(error);
811
1025
  },
812
1026
  onConnectionChange: (connected) => {
1027
+ if (this.sseClient !== client) return;
813
1028
  console.log("[SDK:StreamController] SSE connection change:", connected);
814
1029
  if (connected) {
815
1030
  this.status = "connected";
816
1031
  this.reconnectAttempts = 0;
1032
+ this.stopPolling();
817
1033
  this.resetActivityTimeout();
818
1034
  }
819
- this.emit("connectionChange", { connected, usingFallback: false });
1035
+ this.emit("connectionChange", {
1036
+ connected,
1037
+ usingFallback: this.status === "fallback_polling"
1038
+ });
820
1039
  }
821
1040
  });
1041
+ this.sseClient = client;
822
1042
  try {
823
- await this.sseClient.connect();
1043
+ await client.connect();
824
1044
  console.log("[SDK:StreamController] SSE connect() completed");
825
1045
  } catch (error) {
826
1046
  console.error("[SDK:StreamController] SSE connect() threw:", error);
@@ -1059,6 +1279,7 @@ var ExecutionStreamControllerImpl = class {
1059
1279
  handleComplete(data) {
1060
1280
  this.status = "closed";
1061
1281
  this.stopActivityTimeout();
1282
+ this.stopPolling();
1062
1283
  const sessionId = data.sessionId ?? this.options.sessionId;
1063
1284
  const execution = {
1064
1285
  uuid: this.options.executionId,
@@ -1085,6 +1306,7 @@ var ExecutionStreamControllerImpl = class {
1085
1306
  handleError(error) {
1086
1307
  this.status = "closed";
1087
1308
  this.stopActivityTimeout();
1309
+ this.stopPolling();
1088
1310
  this.finalError = error;
1089
1311
  this.emit("error", error);
1090
1312
  if (this.waitReject) {
@@ -1111,6 +1333,10 @@ var ExecutionStreamControllerImpl = class {
1111
1333
  this.handleError({ error: msg, errorCode: "stream_unrecoverable" });
1112
1334
  return;
1113
1335
  }
1336
+ if (this.status === "fallback_polling") {
1337
+ this.startPolling();
1338
+ return;
1339
+ }
1114
1340
  if (this.reconnectAttempts < this.config.maxReconnectAttempts) {
1115
1341
  this.status = "reconnecting";
1116
1342
  this.reconnectAttempts++;
@@ -1180,42 +1406,216 @@ var ExecutionStreamControllerImpl = class {
1180
1406
  }
1181
1407
  }
1182
1408
  /**
1183
- * Start polling fallback
1409
+ * Start polling fallback.
1410
+ *
1411
+ * Idempotent — calling it while a loop is already running is a no-op, so no
1412
+ * path can end up with two loops racing the same execution.
1184
1413
  */
1185
1414
  startPolling() {
1186
- this.pollingIntervalId = setInterval(async () => {
1187
- try {
1188
- const execution = await this.options.getExecution(this.options.executionId);
1189
- for (const log of execution.logs) {
1190
- if (!this.seenEventIds.has(log.uuid)) {
1191
- this.seenEventIds.add(log.uuid);
1192
- this.emit("log", log);
1193
- }
1194
- }
1195
- if (execution.status === "completed") {
1196
- this.stopPolling();
1197
- this.handleComplete(execution);
1198
- } else if (execution.status === "failed" || execution.status === "cancelled") {
1199
- this.stopPolling();
1200
- this.handleError({
1201
- error: execution.error ?? `Execution ${execution.status}`,
1202
- errorCode: execution.status
1203
- });
1415
+ if (this.pollTimeoutId !== null || this.pollInFlight) {
1416
+ return;
1417
+ }
1418
+ this.pollLastProgressAt = Date.now();
1419
+ this.pollDelayMs = this.pollBaseIntervalMs;
1420
+ this.pollConsecutiveFailures = 0;
1421
+ this.pollConsecutiveRateLimits = 0;
1422
+ this.pollConsecutiveClientErrors = 0;
1423
+ this.lastPolledStatus = null;
1424
+ this.stopActivityTimeout();
1425
+ this.scheduleSSERetry();
1426
+ this.schedulePoll(this.pollDelayMs);
1427
+ }
1428
+ /**
1429
+ * Queue the next poll tick.
1430
+ */
1431
+ schedulePoll(delayMs) {
1432
+ if (this.status !== "fallback_polling") {
1433
+ return;
1434
+ }
1435
+ this.pollTimeoutId = setTimeout(() => {
1436
+ this.pollTimeoutId = null;
1437
+ void this.runPoll();
1438
+ }, Math.max(0, Math.round(delayMs)));
1439
+ }
1440
+ /**
1441
+ * One poll tick: fetch state, emit what is new, then decide how long to wait
1442
+ * before the next one — or whether there should be a next one at all.
1443
+ */
1444
+ async runPoll() {
1445
+ if (this.status !== "fallback_polling") {
1446
+ return;
1447
+ }
1448
+ if (Date.now() - this.pollLastProgressAt >= this.pollMaxDurationMs) {
1449
+ this.failPolling(
1450
+ `Streaming fallback gave up: no execution progress for ${Math.round(this.pollMaxDurationMs / 1e3)}s`,
1451
+ "polling_timeout"
1452
+ );
1453
+ return;
1454
+ }
1455
+ this.pollInFlight = true;
1456
+ try {
1457
+ const execution = await this.options.getExecution(this.options.executionId);
1458
+ this.pollConsecutiveFailures = 0;
1459
+ this.pollConsecutiveRateLimits = 0;
1460
+ this.pollConsecutiveClientErrors = 0;
1461
+ let sawNewInfo = false;
1462
+ for (const log of execution.logs ?? []) {
1463
+ if (!this.seenEventIds.has(log.uuid)) {
1464
+ this.seenEventIds.add(log.uuid);
1465
+ this.emit("log", log);
1466
+ sawNewInfo = true;
1204
1467
  }
1205
- } catch (error) {
1206
- console.error("Polling error:", error);
1207
1468
  }
1208
- }, this.config.pollingIntervalMs);
1469
+ if (execution.status !== this.lastPolledStatus) {
1470
+ this.lastPolledStatus = execution.status;
1471
+ sawNewInfo = true;
1472
+ }
1473
+ if (sawNewInfo) {
1474
+ this.pollLastProgressAt = Date.now();
1475
+ }
1476
+ if (execution.status === "completed") {
1477
+ this.stopPolling();
1478
+ this.handleComplete(execution);
1479
+ return;
1480
+ }
1481
+ if (execution.status === "failed" || execution.status === "cancelled") {
1482
+ this.stopPolling();
1483
+ this.handleError({
1484
+ error: execution.error ?? `Execution ${execution.status}`,
1485
+ errorCode: execution.status
1486
+ });
1487
+ return;
1488
+ }
1489
+ this.pollDelayMs = sawNewInfo ? this.pollBaseIntervalMs : nextIdleDelayMs(this.pollDelayMs, this.pollMaxIntervalMs);
1490
+ } catch (error) {
1491
+ if (!this.handlePollError(error)) {
1492
+ return;
1493
+ }
1494
+ } finally {
1495
+ this.pollInFlight = false;
1496
+ }
1497
+ this.schedulePoll(applyJitter(this.pollDelayMs, this.pollJitterRatio));
1498
+ }
1499
+ /**
1500
+ * Apply backpressure for a failed poll.
1501
+ *
1502
+ * @returns `true` to keep polling (with `pollDelayMs` updated), `false` when
1503
+ * the loop has been terminated.
1504
+ */
1505
+ handlePollError(error) {
1506
+ const retryAfterMs = getRetryAfterMs(error);
1507
+ const rateLimited = isRateLimited(error) || retryAfterMs !== null;
1508
+ if (rateLimited) {
1509
+ this.pollConsecutiveRateLimits++;
1510
+ console.warn(
1511
+ "[SDK:StreamController] Poll rate-limited",
1512
+ retryAfterMs !== null ? `(server asked for ${Math.round(retryAfterMs / 1e3)}s)` : "(no reset named)"
1513
+ );
1514
+ if (retryAfterMs !== null && retryAfterMs > this.pollMaxRetryAfterMs) {
1515
+ this.failPolling(
1516
+ `Rate limited: the API asked for ${Math.round(retryAfterMs / 1e3)}s, longer than this stream will wait`,
1517
+ "rate_limited"
1518
+ );
1519
+ return false;
1520
+ }
1521
+ if (this.pollConsecutiveRateLimits >= this.pollMaxConsecutiveRateLimits) {
1522
+ this.failPolling(
1523
+ "Rate limited by the API; stopped the streaming fallback rather than keep spending the key quota",
1524
+ "rate_limited"
1525
+ );
1526
+ return false;
1527
+ }
1528
+ this.pollDelayMs = Math.max(retryAfterMs ?? UNNAMED_RATE_LIMIT_WAIT_MS, this.pollDelayMs);
1529
+ return true;
1530
+ }
1531
+ if (isTerminalClientError(error)) {
1532
+ this.pollConsecutiveClientErrors++;
1533
+ const status = getHttpStatus(error);
1534
+ console.warn(
1535
+ `[SDK:StreamController] Poll got HTTP ${status} (${this.pollConsecutiveClientErrors}/${this.pollMaxConsecutiveClientErrors})`
1536
+ );
1537
+ if (this.pollConsecutiveClientErrors >= this.pollMaxConsecutiveClientErrors) {
1538
+ this.failPolling(
1539
+ `Execution status endpoint returned HTTP ${status} ${this.pollConsecutiveClientErrors} times \u2014 cannot track this run`,
1540
+ "polling_unrecoverable"
1541
+ );
1542
+ return false;
1543
+ }
1544
+ this.pollDelayMs = nextFailureDelayMs(this.pollDelayMs, this.pollMaxIntervalMs);
1545
+ return true;
1546
+ }
1547
+ this.pollConsecutiveFailures++;
1548
+ console.warn(
1549
+ `[SDK:StreamController] Poll failed (${this.pollConsecutiveFailures}/${this.pollMaxConsecutiveFailures}):`,
1550
+ error
1551
+ );
1552
+ if (this.pollConsecutiveFailures >= this.pollMaxConsecutiveFailures) {
1553
+ this.failPolling(
1554
+ `Streaming fallback failed ${this.pollConsecutiveFailures} times in a row`,
1555
+ "polling_failed"
1556
+ );
1557
+ return false;
1558
+ }
1559
+ this.pollDelayMs = nextFailureDelayMs(this.pollDelayMs, this.pollMaxIntervalMs);
1560
+ return true;
1209
1561
  }
1210
1562
  /**
1211
- * Stop polling
1563
+ * Terminate the fallback loop and surface the reason.
1564
+ */
1565
+ failPolling(error, errorCode) {
1566
+ this.stopPolling();
1567
+ this.handleError({ error, errorCode });
1568
+ }
1569
+ /**
1570
+ * Stop polling, including the periodic SSE re-attempt that belongs to it.
1212
1571
  */
1213
1572
  stopPolling() {
1214
- if (this.pollingIntervalId) {
1215
- clearInterval(this.pollingIntervalId);
1216
- this.pollingIntervalId = null;
1573
+ if (this.pollTimeoutId !== null) {
1574
+ clearTimeout(this.pollTimeoutId);
1575
+ this.pollTimeoutId = null;
1576
+ }
1577
+ if (this.sseRetryTimeoutId !== null) {
1578
+ clearTimeout(this.sseRetryTimeoutId);
1579
+ this.sseRetryTimeoutId = null;
1217
1580
  }
1218
1581
  }
1582
+ /**
1583
+ * Queue the next attempt to get the real stream back.
1584
+ */
1585
+ scheduleSSERetry() {
1586
+ if (this.sseRetryTimeoutId !== null || this.status !== "fallback_polling") {
1587
+ return;
1588
+ }
1589
+ this.sseRetryTimeoutId = setTimeout(() => {
1590
+ this.sseRetryTimeoutId = null;
1591
+ this.attemptSSERecovery();
1592
+ }, this.sseRetryIntervalMs);
1593
+ }
1594
+ /**
1595
+ * Re-attempt SSE from inside the fallback, so a transient outage costs the
1596
+ * session a few polls rather than its stream for the lifetime of the tab.
1597
+ *
1598
+ * Success is NOT observed here: `SSEClient.connect()` resolves when the
1599
+ * stream ENDS, not when it opens. The open is observed by
1600
+ * `onConnectionChange(true)`, which flips status and stops this loop. The
1601
+ * promise below only tells us the attempt is over so the next one can be
1602
+ * queued.
1603
+ */
1604
+ attemptSSERecovery() {
1605
+ if (this.status !== "fallback_polling") {
1606
+ return;
1607
+ }
1608
+ if (this.sseRetryInFlight) {
1609
+ this.scheduleSSERetry();
1610
+ return;
1611
+ }
1612
+ this.sseRetryInFlight = true;
1613
+ void this.connectSSE().catch(() => {
1614
+ }).finally(() => {
1615
+ this.sseRetryInFlight = false;
1616
+ this.scheduleSSERetry();
1617
+ });
1618
+ }
1219
1619
  /**
1220
1620
  * Reset activity timeout
1221
1621
  */
@@ -1346,12 +1746,23 @@ async function collectImagesFromStream(stream, options = {}) {
1346
1746
  });
1347
1747
  }
1348
1748
 
1749
+ // src/utils/api-paths.ts
1750
+ var API_V1 = "/api/v1";
1751
+ var EXECUTION_PATH = `${API_V1}/executions`;
1752
+ function executionPath(executionId) {
1753
+ return `${EXECUTION_PATH}/${executionId}`;
1754
+ }
1755
+ function executionCancelPath(executionId) {
1756
+ return `${EXECUTION_PATH}/${executionId}/cancel`;
1757
+ }
1758
+
1349
1759
  // src/resources/agents.ts
1350
1760
  var AgentsAPI = class extends BaseResourceAPI {
1351
1761
  constructor(http, getStreamConfig) {
1352
1762
  super(http, "/api/v1/agents");
1353
1763
  this.getStreamConfig = getStreamConfig;
1354
1764
  }
1765
+ getStreamConfig;
1355
1766
  // ===== LIST Operations =====
1356
1767
  /**
1357
1768
  * List all agents in the organization
@@ -1608,7 +2019,7 @@ var AgentsAPI = class extends BaseResourceAPI {
1608
2019
  * ```
1609
2020
  */
1610
2021
  async getExecution(executionId) {
1611
- return this.http.get(`/executions/${executionId}`);
2022
+ return this.http.get(executionPath(executionId));
1612
2023
  }
1613
2024
  // ===== EDIT Operations =====
1614
2025
  /**
@@ -1663,7 +2074,7 @@ var AgentsAPI = class extends BaseResourceAPI {
1663
2074
  */
1664
2075
  async cancelExecution(executionId) {
1665
2076
  return this.http.post(
1666
- `/executions/${executionId}/cancel`,
2077
+ executionCancelPath(executionId),
1667
2078
  void 0,
1668
2079
  { skipRetry: true }
1669
2080
  );
@@ -2164,6 +2575,7 @@ var ArtifactsAPI = class extends BaseResourceAPI {
2164
2575
  }
2165
2576
  return this.http.request({
2166
2577
  method: "POST",
2578
+ // KNOWN BROKEN — same missing `/api/v1` prefix as listArtifacts above.
2167
2579
  path: `/executions/${executionId}/artifacts`,
2168
2580
  body: formData,
2169
2581
  headers: {
@@ -2257,6 +2669,7 @@ var SessionsAPI = class extends BaseResourceAPI {
2257
2669
  super(http, "/api/v1/sessions");
2258
2670
  this.getStreamConfig = getStreamConfig;
2259
2671
  }
2672
+ getStreamConfig;
2260
2673
  // ===== LIST Operations =====
2261
2674
  /**
2262
2675
  * List all sessions in the organization
@@ -2436,7 +2849,7 @@ var SessionsAPI = class extends BaseResourceAPI {
2436
2849
  * Get execution result
2437
2850
  */
2438
2851
  async getExecution(executionId) {
2439
- return this.http.get(`/executions/${executionId}`);
2852
+ return this.http.get(executionPath(executionId));
2440
2853
  }
2441
2854
  // ===== EDIT Operations =====
2442
2855
  /**
@@ -2576,6 +2989,7 @@ var KanbanAPI = class extends BaseResourceAPI {
2576
2989
  super(http, "/api/v1/kanban");
2577
2990
  this.getStreamConfig = getStreamConfig;
2578
2991
  }
2992
+ getStreamConfig;
2579
2993
  // ===== BOARD =====
2580
2994
  /**
2581
2995
  * Get the current org's board (lanes in declared order). Auto-provisions
@@ -2710,7 +3124,7 @@ var KanbanAPI = class extends BaseResourceAPI {
2710
3124
  * @returns Execution details.
2711
3125
  */
2712
3126
  async getCardRun(executionId) {
2713
- return this.http.get(`/api/v1/agents/executions/${executionId}`);
3127
+ return this.http.get(executionPath(executionId));
2714
3128
  }
2715
3129
  /**
2716
3130
  * Create a stream controller for a card's run, keyed on the card's
@@ -2777,6 +3191,7 @@ var RemoteAgentsAPI = class extends BaseResourceAPI {
2777
3191
  super(http, "/api/v1/remote-agents");
2778
3192
  this.getStreamConfig = getStreamConfig;
2779
3193
  }
3194
+ getStreamConfig;
2780
3195
  /**
2781
3196
  * List all remote agents in the organization
2782
3197
  *
@@ -2910,7 +3325,7 @@ var RemoteAgentsAPI = class extends BaseResourceAPI {
2910
3325
  apiKey: config.apiKey,
2911
3326
  tokenProvider: config.tokenProvider,
2912
3327
  orgId: config.orgId,
2913
- getExecution: (id) => this.http.get(`/executions/${id}`)
3328
+ getExecution: (id) => this.http.get(executionPath(id))
2914
3329
  });
2915
3330
  return controller;
2916
3331
  }