@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.
@@ -3895,7 +3895,8 @@ var SSEClient = class {
3895
3895
  "Authorization": this.config.authHeader,
3896
3896
  "X-Org-Id": String(this.config.orgId),
3897
3897
  "Accept": "text/event-stream",
3898
- "Cache-Control": "no-cache"
3898
+ "Cache-Control": "no-cache",
3899
+ ...this.config.lastEventId ? { "Last-Event-ID": this.config.lastEventId } : {}
3899
3900
  },
3900
3901
  signal: this.abortController.signal
3901
3902
  });
@@ -4111,13 +4112,167 @@ function normalizeUrl(url) {
4111
4112
  return url;
4112
4113
  }
4113
4114
 
4115
+ // src/streaming/poll-policy.ts
4116
+ var UNNAMED_RATE_LIMIT_WAIT_MS = 6e4;
4117
+ function asDict(value) {
4118
+ return typeof value === "object" && value !== null ? value : null;
4119
+ }
4120
+ var RATE_LIMIT_CODES = /* @__PURE__ */ new Set([
4121
+ "rate_limit_error",
4122
+ "rate_limited",
4123
+ "rate_limit_exceeded",
4124
+ "too_many_requests"
4125
+ ]);
4126
+ function isRateLimited(error) {
4127
+ if (typeof error === "string") return /\b429\b/.test(error);
4128
+ const err = asDict(error);
4129
+ if (!err) return false;
4130
+ for (const key of ["status", "statusCode", "httpStatus", "code"]) {
4131
+ if (err[key] === 429 || err[key] === "429") return true;
4132
+ }
4133
+ const response = asDict(err.response);
4134
+ if (response && (response.status === 429 || response.statusCode === 429)) return true;
4135
+ if (typeof err.code === "string" && RATE_LIMIT_CODES.has(err.code.toLowerCase())) return true;
4136
+ if (err.name === "RateLimitError") return true;
4137
+ const message = typeof err.message === "string" ? err.message : "";
4138
+ return /\b429\b/.test(message) || /rate[\s_-]?limit/i.test(message) || /too many requests/i.test(message);
4139
+ }
4140
+ function readHeader(bag, name) {
4141
+ if (bag === null || bag === void 0) return null;
4142
+ const getter = bag.get;
4143
+ if (typeof getter === "function") {
4144
+ try {
4145
+ const value = getter.call(bag, name);
4146
+ if (value !== null && value !== void 0) return String(value);
4147
+ } catch {
4148
+ }
4149
+ }
4150
+ const dict = asDict(bag);
4151
+ if (!dict) return null;
4152
+ const wanted = name.toLowerCase();
4153
+ for (const [key, value] of Object.entries(dict)) {
4154
+ if (key.toLowerCase() === wanted && value !== null && value !== void 0) {
4155
+ return String(value);
4156
+ }
4157
+ }
4158
+ return null;
4159
+ }
4160
+ function parseRetryAfter(raw, now) {
4161
+ if (raw === null || raw === void 0) return null;
4162
+ if (typeof raw === "number") {
4163
+ return Number.isFinite(raw) && raw >= 0 ? raw * 1e3 : null;
4164
+ }
4165
+ if (typeof raw !== "string") return null;
4166
+ const trimmed = raw.trim();
4167
+ if (trimmed === "") return null;
4168
+ if (/^\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed) * 1e3;
4169
+ const at = Date.parse(trimmed);
4170
+ return Number.isNaN(at) ? null : Math.max(0, at - now);
4171
+ }
4172
+ function parseResetAt(raw, now) {
4173
+ if (raw === null || raw === void 0) return null;
4174
+ const numeric = typeof raw === "number" ? raw : typeof raw === "string" && /^\d+(\.\d+)?$/.test(raw.trim()) ? Number(raw.trim()) : null;
4175
+ if (numeric !== null) {
4176
+ if (!Number.isFinite(numeric) || numeric < 0) return null;
4177
+ if (numeric > 1e11) return Math.max(0, numeric - now);
4178
+ if (numeric > 1e9) return Math.max(0, numeric * 1e3 - now);
4179
+ return numeric * 1e3;
4180
+ }
4181
+ if (typeof raw !== "string") return null;
4182
+ const at = Date.parse(raw.trim());
4183
+ return Number.isNaN(at) ? null : Math.max(0, at - now);
4184
+ }
4185
+ function getRetryAfterMs(error, now = Date.now()) {
4186
+ const err = asDict(error);
4187
+ if (!err) return null;
4188
+ const details = asDict(err.details);
4189
+ const response = asDict(err.response);
4190
+ const body = asDict(err.body) ?? asDict(response?.data);
4191
+ const bodyError = asDict(body?.error);
4192
+ const bodyDetails = asDict(bodyError?.details) ?? asDict(body?.details);
4193
+ for (const source of [err, details, bodyDetails]) {
4194
+ const ms = source?.retryAfterMs;
4195
+ if (typeof ms === "number" && Number.isFinite(ms) && ms >= 0) return ms;
4196
+ }
4197
+ for (const source of [err, details, bodyDetails, bodyError]) {
4198
+ const parsed = parseRetryAfter(source?.retryAfter, now);
4199
+ if (parsed !== null) return parsed;
4200
+ }
4201
+ const headerBags = [err.headers, response?.headers, details?.headers, body?.headers];
4202
+ for (const bag of headerBags) {
4203
+ const parsed = parseRetryAfter(readHeader(bag, "retry-after"), now);
4204
+ if (parsed !== null) return parsed;
4205
+ }
4206
+ for (const source of [details, bodyDetails, err]) {
4207
+ for (const key of ["resetAt", "reset", "rateLimitReset"]) {
4208
+ const parsed = parseResetAt(source?.[key], now);
4209
+ if (parsed !== null) return parsed;
4210
+ }
4211
+ }
4212
+ for (const bag of headerBags) {
4213
+ for (const header of ["x-ratelimit-reset", "ratelimit-reset"]) {
4214
+ const parsed = parseResetAt(readHeader(bag, header), now);
4215
+ if (parsed !== null) return parsed;
4216
+ }
4217
+ }
4218
+ return null;
4219
+ }
4220
+ function getHttpStatus(error) {
4221
+ const err = asDict(error);
4222
+ if (!err) return null;
4223
+ for (const key of ["status", "statusCode", "httpStatus"]) {
4224
+ const value = err[key];
4225
+ if (typeof value === "number" && Number.isFinite(value)) return value;
4226
+ if (typeof value === "string" && /^\d{3}$/.test(value)) return Number(value);
4227
+ }
4228
+ const response = asDict(err.response);
4229
+ for (const key of ["status", "statusCode"]) {
4230
+ const value = response?.[key];
4231
+ if (typeof value === "number" && Number.isFinite(value)) return value;
4232
+ }
4233
+ return null;
4234
+ }
4235
+ function isTerminalClientError(error) {
4236
+ const status = getHttpStatus(error);
4237
+ if (status === null) return false;
4238
+ return status >= 400 && status < 500 && status !== 429 && status !== 408;
4239
+ }
4240
+ var FAILURE_GROWTH_FACTOR = 2;
4241
+ var IDLE_GROWTH_FACTOR = 1.5;
4242
+ function nextFailureDelayMs(currentMs, maxMs) {
4243
+ return Math.min(Math.max(currentMs, 1) * FAILURE_GROWTH_FACTOR, maxMs);
4244
+ }
4245
+ function nextIdleDelayMs(currentMs, maxMs) {
4246
+ return Math.min(Math.ceil(Math.max(currentMs, 1) * IDLE_GROWTH_FACTOR), maxMs);
4247
+ }
4248
+ function applyJitter(delayMs, ratio, random = Math.random) {
4249
+ if (ratio <= 0) return delayMs;
4250
+ return Math.round(delayMs * (1 + random() * ratio));
4251
+ }
4252
+
4114
4253
  // src/streaming/stream-controller.ts
4115
4254
  var DEFAULT_STREAMING_CONFIG = {
4116
4255
  maxReconnectAttempts: 5,
4117
4256
  reconnectBaseDelayMs: 1e3,
4118
4257
  reconnectMaxDelayMs: 1e4,
4258
+ // NOTE (unchanged, deliberately): this watchdog force-disconnects a HEALTHY
4259
+ // SSE connection after 30s of silence and routes it through the same
4260
+ // handleConnectionError path as a real transport failure. Ordinary LLM
4261
+ // latency exceeds 30s routinely, so it fires against working streams — that
4262
+ // is what drives sessions into the polling fallback in the first place. The
4263
+ // fix is a server-side SSE heartbeat, not a bigger number here; raising this
4264
+ // is a guess against a moving target. Everything below only bounds what the
4265
+ // fallback costs once it has been entered.
4119
4266
  activityTimeoutMs: 3e4,
4120
- pollingIntervalMs: 2e3
4267
+ pollingIntervalMs: 5e3,
4268
+ pollingMaxIntervalMs: 3e4,
4269
+ pollingMaxConsecutiveFailures: 10,
4270
+ pollingMaxConsecutiveRateLimits: 3,
4271
+ pollingMaxConsecutiveClientErrors: 3,
4272
+ pollingMaxDurationMs: 18e5,
4273
+ pollingMaxRetryAfterMs: 3e5,
4274
+ pollingJitterRatio: 0.1,
4275
+ sseRetryIntervalMs: 6e4
4121
4276
  };
4122
4277
  var DEFAULT_POLLING_ENABLED = true;
4123
4278
  var ExecutionStreamControllerImpl = class {
@@ -4130,9 +4285,24 @@ var ExecutionStreamControllerImpl = class {
4130
4285
  seenEventIds = /* @__PURE__ */ new Set();
4131
4286
  _lastEventId = null;
4132
4287
  activityTimeoutId = null;
4133
- pollingIntervalId = null;
4134
4288
  finalExecution = null;
4135
4289
  finalError = null;
4290
+ // --- Fallback polling state ---
4291
+ // A self-rescheduling timeout, not setInterval: the delay has to change
4292
+ // between ticks (that is what backoff IS), and a timeout chain also cannot
4293
+ // stack overlapping requests the way a fixed interval does when the API is
4294
+ // slow to answer.
4295
+ pollTimeoutId = null;
4296
+ pollDelayMs = 0;
4297
+ pollConsecutiveFailures = 0;
4298
+ pollConsecutiveRateLimits = 0;
4299
+ pollConsecutiveClientErrors = 0;
4300
+ /** When the loop last observed anything new — the clock the give-up bound runs on. */
4301
+ pollLastProgressAt = 0;
4302
+ pollInFlight = false;
4303
+ lastPolledStatus = null;
4304
+ sseRetryTimeoutId = null;
4305
+ sseRetryInFlight = false;
4136
4306
  // Event callbacks
4137
4307
  listeners = {
4138
4308
  log: [],
@@ -4161,6 +4331,39 @@ var ExecutionStreamControllerImpl = class {
4161
4331
  this.config = { ...DEFAULT_STREAMING_CONFIG, ...options.config };
4162
4332
  this.pollingEnabled = options.pollingEnabled ?? DEFAULT_POLLING_ENABLED;
4163
4333
  }
4334
+ // Resolved fallback-poll settings. Read through `??` rather than off the
4335
+ // spread config so an explicit `undefined` in a caller's partial config still
4336
+ // lands on the default instead of NaN-ing the arithmetic.
4337
+ get pollBaseIntervalMs() {
4338
+ return this.config.pollingIntervalMs ?? DEFAULT_STREAMING_CONFIG.pollingIntervalMs;
4339
+ }
4340
+ get pollMaxIntervalMs() {
4341
+ return Math.max(
4342
+ this.config.pollingMaxIntervalMs ?? DEFAULT_STREAMING_CONFIG.pollingMaxIntervalMs,
4343
+ this.pollBaseIntervalMs
4344
+ );
4345
+ }
4346
+ get pollMaxConsecutiveFailures() {
4347
+ return this.config.pollingMaxConsecutiveFailures ?? DEFAULT_STREAMING_CONFIG.pollingMaxConsecutiveFailures;
4348
+ }
4349
+ get pollMaxConsecutiveRateLimits() {
4350
+ return this.config.pollingMaxConsecutiveRateLimits ?? DEFAULT_STREAMING_CONFIG.pollingMaxConsecutiveRateLimits;
4351
+ }
4352
+ get pollMaxConsecutiveClientErrors() {
4353
+ return this.config.pollingMaxConsecutiveClientErrors ?? DEFAULT_STREAMING_CONFIG.pollingMaxConsecutiveClientErrors;
4354
+ }
4355
+ get pollMaxDurationMs() {
4356
+ return this.config.pollingMaxDurationMs ?? DEFAULT_STREAMING_CONFIG.pollingMaxDurationMs;
4357
+ }
4358
+ get pollMaxRetryAfterMs() {
4359
+ return this.config.pollingMaxRetryAfterMs ?? DEFAULT_STREAMING_CONFIG.pollingMaxRetryAfterMs;
4360
+ }
4361
+ get pollJitterRatio() {
4362
+ return this.config.pollingJitterRatio ?? DEFAULT_STREAMING_CONFIG.pollingJitterRatio;
4363
+ }
4364
+ get sseRetryIntervalMs() {
4365
+ return this.config.sseRetryIntervalMs ?? DEFAULT_STREAMING_CONFIG.sseRetryIntervalMs;
4366
+ }
4164
4367
  /**
4165
4368
  * Get the last event ID (for resume support)
4166
4369
  */
@@ -4251,28 +4454,45 @@ var ExecutionStreamControllerImpl = class {
4251
4454
  console.log("[SDK:StreamController] executionId:", this.options.executionId);
4252
4455
  console.log("[SDK:StreamController] orgId:", this.options.orgId);
4253
4456
  console.log("[SDK:StreamController] apiKey:", authToken ? `${authToken.substring(0, 20)}...` : "MISSING");
4254
- this.sseClient = new SSEClient({
4457
+ if (this.sseClient) {
4458
+ this.sseClient.disconnect();
4459
+ this.sseClient = null;
4460
+ }
4461
+ const client = new SSEClient({
4255
4462
  url,
4256
4463
  authHeader: `Bearer ${authToken}`,
4257
4464
  orgId: this.options.orgId,
4258
4465
  fetch: this.options.fetch,
4259
- onEvent: (event) => this.handleEvent(event),
4466
+ // Resume from where this session left off. On a first connect this is
4467
+ // null and the header is omitted.
4468
+ lastEventId: this._lastEventId,
4469
+ onEvent: (event) => {
4470
+ if (this.sseClient !== client) return;
4471
+ this.handleEvent(event);
4472
+ },
4260
4473
  onError: (error) => {
4474
+ if (this.sseClient !== client) return;
4261
4475
  console.error("[SDK:StreamController] SSE error:", error);
4262
4476
  this.handleConnectionError(error);
4263
4477
  },
4264
4478
  onConnectionChange: (connected) => {
4479
+ if (this.sseClient !== client) return;
4265
4480
  console.log("[SDK:StreamController] SSE connection change:", connected);
4266
4481
  if (connected) {
4267
4482
  this.status = "connected";
4268
4483
  this.reconnectAttempts = 0;
4484
+ this.stopPolling();
4269
4485
  this.resetActivityTimeout();
4270
4486
  }
4271
- this.emit("connectionChange", { connected, usingFallback: false });
4487
+ this.emit("connectionChange", {
4488
+ connected,
4489
+ usingFallback: this.status === "fallback_polling"
4490
+ });
4272
4491
  }
4273
4492
  });
4493
+ this.sseClient = client;
4274
4494
  try {
4275
- await this.sseClient.connect();
4495
+ await client.connect();
4276
4496
  console.log("[SDK:StreamController] SSE connect() completed");
4277
4497
  } catch (error) {
4278
4498
  console.error("[SDK:StreamController] SSE connect() threw:", error);
@@ -4511,6 +4731,7 @@ var ExecutionStreamControllerImpl = class {
4511
4731
  handleComplete(data) {
4512
4732
  this.status = "closed";
4513
4733
  this.stopActivityTimeout();
4734
+ this.stopPolling();
4514
4735
  const sessionId = data.sessionId ?? this.options.sessionId;
4515
4736
  const execution = {
4516
4737
  uuid: this.options.executionId,
@@ -4537,6 +4758,7 @@ var ExecutionStreamControllerImpl = class {
4537
4758
  handleError(error) {
4538
4759
  this.status = "closed";
4539
4760
  this.stopActivityTimeout();
4761
+ this.stopPolling();
4540
4762
  this.finalError = error;
4541
4763
  this.emit("error", error);
4542
4764
  if (this.waitReject) {
@@ -4563,6 +4785,10 @@ var ExecutionStreamControllerImpl = class {
4563
4785
  this.handleError({ error: msg, errorCode: "stream_unrecoverable" });
4564
4786
  return;
4565
4787
  }
4788
+ if (this.status === "fallback_polling") {
4789
+ this.startPolling();
4790
+ return;
4791
+ }
4566
4792
  if (this.reconnectAttempts < this.config.maxReconnectAttempts) {
4567
4793
  this.status = "reconnecting";
4568
4794
  this.reconnectAttempts++;
@@ -4632,41 +4858,215 @@ var ExecutionStreamControllerImpl = class {
4632
4858
  }
4633
4859
  }
4634
4860
  /**
4635
- * Start polling fallback
4861
+ * Start polling fallback.
4862
+ *
4863
+ * Idempotent — calling it while a loop is already running is a no-op, so no
4864
+ * path can end up with two loops racing the same execution.
4636
4865
  */
4637
4866
  startPolling() {
4638
- this.pollingIntervalId = setInterval(async () => {
4639
- try {
4640
- const execution = await this.options.getExecution(this.options.executionId);
4641
- for (const log of execution.logs) {
4642
- if (!this.seenEventIds.has(log.uuid)) {
4643
- this.seenEventIds.add(log.uuid);
4644
- this.emit("log", log);
4645
- }
4646
- }
4647
- if (execution.status === "completed") {
4648
- this.stopPolling();
4649
- this.handleComplete(execution);
4650
- } else if (execution.status === "failed" || execution.status === "cancelled") {
4651
- this.stopPolling();
4652
- this.handleError({
4653
- error: execution.error ?? `Execution ${execution.status}`,
4654
- errorCode: execution.status
4655
- });
4867
+ if (this.pollTimeoutId !== null || this.pollInFlight) {
4868
+ return;
4869
+ }
4870
+ this.pollLastProgressAt = Date.now();
4871
+ this.pollDelayMs = this.pollBaseIntervalMs;
4872
+ this.pollConsecutiveFailures = 0;
4873
+ this.pollConsecutiveRateLimits = 0;
4874
+ this.pollConsecutiveClientErrors = 0;
4875
+ this.lastPolledStatus = null;
4876
+ this.stopActivityTimeout();
4877
+ this.scheduleSSERetry();
4878
+ this.schedulePoll(this.pollDelayMs);
4879
+ }
4880
+ /**
4881
+ * Queue the next poll tick.
4882
+ */
4883
+ schedulePoll(delayMs) {
4884
+ if (this.status !== "fallback_polling") {
4885
+ return;
4886
+ }
4887
+ this.pollTimeoutId = setTimeout(() => {
4888
+ this.pollTimeoutId = null;
4889
+ void this.runPoll();
4890
+ }, Math.max(0, Math.round(delayMs)));
4891
+ }
4892
+ /**
4893
+ * One poll tick: fetch state, emit what is new, then decide how long to wait
4894
+ * before the next one — or whether there should be a next one at all.
4895
+ */
4896
+ async runPoll() {
4897
+ if (this.status !== "fallback_polling") {
4898
+ return;
4899
+ }
4900
+ if (Date.now() - this.pollLastProgressAt >= this.pollMaxDurationMs) {
4901
+ this.failPolling(
4902
+ `Streaming fallback gave up: no execution progress for ${Math.round(this.pollMaxDurationMs / 1e3)}s`,
4903
+ "polling_timeout"
4904
+ );
4905
+ return;
4906
+ }
4907
+ this.pollInFlight = true;
4908
+ try {
4909
+ const execution = await this.options.getExecution(this.options.executionId);
4910
+ this.pollConsecutiveFailures = 0;
4911
+ this.pollConsecutiveRateLimits = 0;
4912
+ this.pollConsecutiveClientErrors = 0;
4913
+ let sawNewInfo = false;
4914
+ for (const log of execution.logs ?? []) {
4915
+ if (!this.seenEventIds.has(log.uuid)) {
4916
+ this.seenEventIds.add(log.uuid);
4917
+ this.emit("log", log);
4918
+ sawNewInfo = true;
4656
4919
  }
4657
- } catch (error) {
4658
- console.error("Polling error:", error);
4659
4920
  }
4660
- }, this.config.pollingIntervalMs);
4921
+ if (execution.status !== this.lastPolledStatus) {
4922
+ this.lastPolledStatus = execution.status;
4923
+ sawNewInfo = true;
4924
+ }
4925
+ if (sawNewInfo) {
4926
+ this.pollLastProgressAt = Date.now();
4927
+ }
4928
+ if (execution.status === "completed") {
4929
+ this.stopPolling();
4930
+ this.handleComplete(execution);
4931
+ return;
4932
+ }
4933
+ if (execution.status === "failed" || execution.status === "cancelled") {
4934
+ this.stopPolling();
4935
+ this.handleError({
4936
+ error: execution.error ?? `Execution ${execution.status}`,
4937
+ errorCode: execution.status
4938
+ });
4939
+ return;
4940
+ }
4941
+ this.pollDelayMs = sawNewInfo ? this.pollBaseIntervalMs : nextIdleDelayMs(this.pollDelayMs, this.pollMaxIntervalMs);
4942
+ } catch (error) {
4943
+ if (!this.handlePollError(error)) {
4944
+ return;
4945
+ }
4946
+ } finally {
4947
+ this.pollInFlight = false;
4948
+ }
4949
+ this.schedulePoll(applyJitter(this.pollDelayMs, this.pollJitterRatio));
4950
+ }
4951
+ /**
4952
+ * Apply backpressure for a failed poll.
4953
+ *
4954
+ * @returns `true` to keep polling (with `pollDelayMs` updated), `false` when
4955
+ * the loop has been terminated.
4956
+ */
4957
+ handlePollError(error) {
4958
+ const retryAfterMs = getRetryAfterMs(error);
4959
+ const rateLimited = isRateLimited(error) || retryAfterMs !== null;
4960
+ if (rateLimited) {
4961
+ this.pollConsecutiveRateLimits++;
4962
+ console.warn(
4963
+ "[SDK:StreamController] Poll rate-limited",
4964
+ retryAfterMs !== null ? `(server asked for ${Math.round(retryAfterMs / 1e3)}s)` : "(no reset named)"
4965
+ );
4966
+ if (retryAfterMs !== null && retryAfterMs > this.pollMaxRetryAfterMs) {
4967
+ this.failPolling(
4968
+ `Rate limited: the API asked for ${Math.round(retryAfterMs / 1e3)}s, longer than this stream will wait`,
4969
+ "rate_limited"
4970
+ );
4971
+ return false;
4972
+ }
4973
+ if (this.pollConsecutiveRateLimits >= this.pollMaxConsecutiveRateLimits) {
4974
+ this.failPolling(
4975
+ "Rate limited by the API; stopped the streaming fallback rather than keep spending the key quota",
4976
+ "rate_limited"
4977
+ );
4978
+ return false;
4979
+ }
4980
+ this.pollDelayMs = Math.max(retryAfterMs ?? UNNAMED_RATE_LIMIT_WAIT_MS, this.pollDelayMs);
4981
+ return true;
4982
+ }
4983
+ if (isTerminalClientError(error)) {
4984
+ this.pollConsecutiveClientErrors++;
4985
+ const status = getHttpStatus(error);
4986
+ console.warn(
4987
+ `[SDK:StreamController] Poll got HTTP ${status} (${this.pollConsecutiveClientErrors}/${this.pollMaxConsecutiveClientErrors})`
4988
+ );
4989
+ if (this.pollConsecutiveClientErrors >= this.pollMaxConsecutiveClientErrors) {
4990
+ this.failPolling(
4991
+ `Execution status endpoint returned HTTP ${status} ${this.pollConsecutiveClientErrors} times \u2014 cannot track this run`,
4992
+ "polling_unrecoverable"
4993
+ );
4994
+ return false;
4995
+ }
4996
+ this.pollDelayMs = nextFailureDelayMs(this.pollDelayMs, this.pollMaxIntervalMs);
4997
+ return true;
4998
+ }
4999
+ this.pollConsecutiveFailures++;
5000
+ console.warn(
5001
+ `[SDK:StreamController] Poll failed (${this.pollConsecutiveFailures}/${this.pollMaxConsecutiveFailures}):`,
5002
+ error
5003
+ );
5004
+ if (this.pollConsecutiveFailures >= this.pollMaxConsecutiveFailures) {
5005
+ this.failPolling(
5006
+ `Streaming fallback failed ${this.pollConsecutiveFailures} times in a row`,
5007
+ "polling_failed"
5008
+ );
5009
+ return false;
5010
+ }
5011
+ this.pollDelayMs = nextFailureDelayMs(this.pollDelayMs, this.pollMaxIntervalMs);
5012
+ return true;
5013
+ }
5014
+ /**
5015
+ * Terminate the fallback loop and surface the reason.
5016
+ */
5017
+ failPolling(error, errorCode) {
5018
+ this.stopPolling();
5019
+ this.handleError({ error, errorCode });
4661
5020
  }
4662
5021
  /**
4663
- * Stop polling
5022
+ * Stop polling, including the periodic SSE re-attempt that belongs to it.
4664
5023
  */
4665
5024
  stopPolling() {
4666
- if (this.pollingIntervalId) {
4667
- clearInterval(this.pollingIntervalId);
4668
- this.pollingIntervalId = null;
5025
+ if (this.pollTimeoutId !== null) {
5026
+ clearTimeout(this.pollTimeoutId);
5027
+ this.pollTimeoutId = null;
5028
+ }
5029
+ if (this.sseRetryTimeoutId !== null) {
5030
+ clearTimeout(this.sseRetryTimeoutId);
5031
+ this.sseRetryTimeoutId = null;
5032
+ }
5033
+ }
5034
+ /**
5035
+ * Queue the next attempt to get the real stream back.
5036
+ */
5037
+ scheduleSSERetry() {
5038
+ if (this.sseRetryTimeoutId !== null || this.status !== "fallback_polling") {
5039
+ return;
4669
5040
  }
5041
+ this.sseRetryTimeoutId = setTimeout(() => {
5042
+ this.sseRetryTimeoutId = null;
5043
+ this.attemptSSERecovery();
5044
+ }, this.sseRetryIntervalMs);
5045
+ }
5046
+ /**
5047
+ * Re-attempt SSE from inside the fallback, so a transient outage costs the
5048
+ * session a few polls rather than its stream for the lifetime of the tab.
5049
+ *
5050
+ * Success is NOT observed here: `SSEClient.connect()` resolves when the
5051
+ * stream ENDS, not when it opens. The open is observed by
5052
+ * `onConnectionChange(true)`, which flips status and stops this loop. The
5053
+ * promise below only tells us the attempt is over so the next one can be
5054
+ * queued.
5055
+ */
5056
+ attemptSSERecovery() {
5057
+ if (this.status !== "fallback_polling") {
5058
+ return;
5059
+ }
5060
+ if (this.sseRetryInFlight) {
5061
+ this.scheduleSSERetry();
5062
+ return;
5063
+ }
5064
+ this.sseRetryInFlight = true;
5065
+ void this.connectSSE().catch(() => {
5066
+ }).finally(() => {
5067
+ this.sseRetryInFlight = false;
5068
+ this.scheduleSSERetry();
5069
+ });
4670
5070
  }
4671
5071
  /**
4672
5072
  * Reset activity timeout
@@ -4798,12 +5198,23 @@ async function collectImagesFromStream(stream, options = {}) {
4798
5198
  });
4799
5199
  }
4800
5200
 
5201
+ // src/utils/api-paths.ts
5202
+ var API_V1 = "/api/v1";
5203
+ var EXECUTION_PATH = `${API_V1}/executions`;
5204
+ function executionPath(executionId) {
5205
+ return `${EXECUTION_PATH}/${executionId}`;
5206
+ }
5207
+ function executionCancelPath(executionId) {
5208
+ return `${EXECUTION_PATH}/${executionId}/cancel`;
5209
+ }
5210
+
4801
5211
  // src/resources/agents.ts
4802
5212
  var AgentsAPI = class extends BaseResourceAPI {
4803
5213
  constructor(http, getStreamConfig) {
4804
5214
  super(http, "/api/v1/agents");
4805
5215
  this.getStreamConfig = getStreamConfig;
4806
5216
  }
5217
+ getStreamConfig;
4807
5218
  // ===== LIST Operations =====
4808
5219
  /**
4809
5220
  * List all agents in the organization
@@ -5060,7 +5471,7 @@ var AgentsAPI = class extends BaseResourceAPI {
5060
5471
  * ```
5061
5472
  */
5062
5473
  async getExecution(executionId) {
5063
- return this.http.get(`/executions/${executionId}`);
5474
+ return this.http.get(executionPath(executionId));
5064
5475
  }
5065
5476
  // ===== EDIT Operations =====
5066
5477
  /**
@@ -5115,7 +5526,7 @@ var AgentsAPI = class extends BaseResourceAPI {
5115
5526
  */
5116
5527
  async cancelExecution(executionId) {
5117
5528
  return this.http.post(
5118
- `/executions/${executionId}/cancel`,
5529
+ executionCancelPath(executionId),
5119
5530
  void 0,
5120
5531
  { skipRetry: true }
5121
5532
  );
@@ -5616,6 +6027,7 @@ var ArtifactsAPI = class extends BaseResourceAPI {
5616
6027
  }
5617
6028
  return this.http.request({
5618
6029
  method: "POST",
6030
+ // KNOWN BROKEN — same missing `/api/v1` prefix as listArtifacts above.
5619
6031
  path: `/executions/${executionId}/artifacts`,
5620
6032
  body: formData,
5621
6033
  headers: {
@@ -5709,6 +6121,7 @@ var SessionsAPI = class extends BaseResourceAPI {
5709
6121
  super(http, "/api/v1/sessions");
5710
6122
  this.getStreamConfig = getStreamConfig;
5711
6123
  }
6124
+ getStreamConfig;
5712
6125
  // ===== LIST Operations =====
5713
6126
  /**
5714
6127
  * List all sessions in the organization
@@ -5888,7 +6301,7 @@ var SessionsAPI = class extends BaseResourceAPI {
5888
6301
  * Get execution result
5889
6302
  */
5890
6303
  async getExecution(executionId) {
5891
- return this.http.get(`/executions/${executionId}`);
6304
+ return this.http.get(executionPath(executionId));
5892
6305
  }
5893
6306
  // ===== EDIT Operations =====
5894
6307
  /**
@@ -6028,6 +6441,7 @@ var KanbanAPI = class extends BaseResourceAPI {
6028
6441
  super(http, "/api/v1/kanban");
6029
6442
  this.getStreamConfig = getStreamConfig;
6030
6443
  }
6444
+ getStreamConfig;
6031
6445
  // ===== BOARD =====
6032
6446
  /**
6033
6447
  * Get the current org's board (lanes in declared order). Auto-provisions
@@ -6162,7 +6576,7 @@ var KanbanAPI = class extends BaseResourceAPI {
6162
6576
  * @returns Execution details.
6163
6577
  */
6164
6578
  async getCardRun(executionId) {
6165
- return this.http.get(`/api/v1/agents/executions/${executionId}`);
6579
+ return this.http.get(executionPath(executionId));
6166
6580
  }
6167
6581
  /**
6168
6582
  * Create a stream controller for a card's run, keyed on the card's
@@ -6229,6 +6643,7 @@ var RemoteAgentsAPI = class extends BaseResourceAPI {
6229
6643
  super(http, "/api/v1/remote-agents");
6230
6644
  this.getStreamConfig = getStreamConfig;
6231
6645
  }
6646
+ getStreamConfig;
6232
6647
  /**
6233
6648
  * List all remote agents in the organization
6234
6649
  *
@@ -6362,7 +6777,7 @@ var RemoteAgentsAPI = class extends BaseResourceAPI {
6362
6777
  apiKey: config.apiKey,
6363
6778
  tokenProvider: config.tokenProvider,
6364
6779
  orgId: config.orgId,
6365
- getExecution: (id) => this.http.get(`/executions/${id}`)
6780
+ getExecution: (id) => this.http.get(executionPath(id))
6366
6781
  });
6367
6782
  return controller;
6368
6783
  }