@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.
@@ -3888,7 +3888,8 @@ var SSEClient = class {
3888
3888
  "Authorization": this.config.authHeader,
3889
3889
  "X-Org-Id": String(this.config.orgId),
3890
3890
  "Accept": "text/event-stream",
3891
- "Cache-Control": "no-cache"
3891
+ "Cache-Control": "no-cache",
3892
+ ...this.config.lastEventId ? { "Last-Event-ID": this.config.lastEventId } : {}
3892
3893
  },
3893
3894
  signal: this.abortController.signal
3894
3895
  });
@@ -4104,13 +4105,167 @@ function normalizeUrl(url) {
4104
4105
  return url;
4105
4106
  }
4106
4107
 
4108
+ // src/streaming/poll-policy.ts
4109
+ var UNNAMED_RATE_LIMIT_WAIT_MS = 6e4;
4110
+ function asDict(value) {
4111
+ return typeof value === "object" && value !== null ? value : null;
4112
+ }
4113
+ var RATE_LIMIT_CODES = /* @__PURE__ */ new Set([
4114
+ "rate_limit_error",
4115
+ "rate_limited",
4116
+ "rate_limit_exceeded",
4117
+ "too_many_requests"
4118
+ ]);
4119
+ function isRateLimited(error) {
4120
+ if (typeof error === "string") return /\b429\b/.test(error);
4121
+ const err = asDict(error);
4122
+ if (!err) return false;
4123
+ for (const key of ["status", "statusCode", "httpStatus", "code"]) {
4124
+ if (err[key] === 429 || err[key] === "429") return true;
4125
+ }
4126
+ const response = asDict(err.response);
4127
+ if (response && (response.status === 429 || response.statusCode === 429)) return true;
4128
+ if (typeof err.code === "string" && RATE_LIMIT_CODES.has(err.code.toLowerCase())) return true;
4129
+ if (err.name === "RateLimitError") return true;
4130
+ const message = typeof err.message === "string" ? err.message : "";
4131
+ return /\b429\b/.test(message) || /rate[\s_-]?limit/i.test(message) || /too many requests/i.test(message);
4132
+ }
4133
+ function readHeader(bag, name) {
4134
+ if (bag === null || bag === void 0) return null;
4135
+ const getter = bag.get;
4136
+ if (typeof getter === "function") {
4137
+ try {
4138
+ const value = getter.call(bag, name);
4139
+ if (value !== null && value !== void 0) return String(value);
4140
+ } catch {
4141
+ }
4142
+ }
4143
+ const dict = asDict(bag);
4144
+ if (!dict) return null;
4145
+ const wanted = name.toLowerCase();
4146
+ for (const [key, value] of Object.entries(dict)) {
4147
+ if (key.toLowerCase() === wanted && value !== null && value !== void 0) {
4148
+ return String(value);
4149
+ }
4150
+ }
4151
+ return null;
4152
+ }
4153
+ function parseRetryAfter(raw, now) {
4154
+ if (raw === null || raw === void 0) return null;
4155
+ if (typeof raw === "number") {
4156
+ return Number.isFinite(raw) && raw >= 0 ? raw * 1e3 : null;
4157
+ }
4158
+ if (typeof raw !== "string") return null;
4159
+ const trimmed = raw.trim();
4160
+ if (trimmed === "") return null;
4161
+ if (/^\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed) * 1e3;
4162
+ const at = Date.parse(trimmed);
4163
+ return Number.isNaN(at) ? null : Math.max(0, at - now);
4164
+ }
4165
+ function parseResetAt(raw, now) {
4166
+ if (raw === null || raw === void 0) return null;
4167
+ const numeric = typeof raw === "number" ? raw : typeof raw === "string" && /^\d+(\.\d+)?$/.test(raw.trim()) ? Number(raw.trim()) : null;
4168
+ if (numeric !== null) {
4169
+ if (!Number.isFinite(numeric) || numeric < 0) return null;
4170
+ if (numeric > 1e11) return Math.max(0, numeric - now);
4171
+ if (numeric > 1e9) return Math.max(0, numeric * 1e3 - now);
4172
+ return numeric * 1e3;
4173
+ }
4174
+ if (typeof raw !== "string") return null;
4175
+ const at = Date.parse(raw.trim());
4176
+ return Number.isNaN(at) ? null : Math.max(0, at - now);
4177
+ }
4178
+ function getRetryAfterMs(error, now = Date.now()) {
4179
+ const err = asDict(error);
4180
+ if (!err) return null;
4181
+ const details = asDict(err.details);
4182
+ const response = asDict(err.response);
4183
+ const body = asDict(err.body) ?? asDict(response?.data);
4184
+ const bodyError = asDict(body?.error);
4185
+ const bodyDetails = asDict(bodyError?.details) ?? asDict(body?.details);
4186
+ for (const source of [err, details, bodyDetails]) {
4187
+ const ms = source?.retryAfterMs;
4188
+ if (typeof ms === "number" && Number.isFinite(ms) && ms >= 0) return ms;
4189
+ }
4190
+ for (const source of [err, details, bodyDetails, bodyError]) {
4191
+ const parsed = parseRetryAfter(source?.retryAfter, now);
4192
+ if (parsed !== null) return parsed;
4193
+ }
4194
+ const headerBags = [err.headers, response?.headers, details?.headers, body?.headers];
4195
+ for (const bag of headerBags) {
4196
+ const parsed = parseRetryAfter(readHeader(bag, "retry-after"), now);
4197
+ if (parsed !== null) return parsed;
4198
+ }
4199
+ for (const source of [details, bodyDetails, err]) {
4200
+ for (const key of ["resetAt", "reset", "rateLimitReset"]) {
4201
+ const parsed = parseResetAt(source?.[key], now);
4202
+ if (parsed !== null) return parsed;
4203
+ }
4204
+ }
4205
+ for (const bag of headerBags) {
4206
+ for (const header of ["x-ratelimit-reset", "ratelimit-reset"]) {
4207
+ const parsed = parseResetAt(readHeader(bag, header), now);
4208
+ if (parsed !== null) return parsed;
4209
+ }
4210
+ }
4211
+ return null;
4212
+ }
4213
+ function getHttpStatus(error) {
4214
+ const err = asDict(error);
4215
+ if (!err) return null;
4216
+ for (const key of ["status", "statusCode", "httpStatus"]) {
4217
+ const value = err[key];
4218
+ if (typeof value === "number" && Number.isFinite(value)) return value;
4219
+ if (typeof value === "string" && /^\d{3}$/.test(value)) return Number(value);
4220
+ }
4221
+ const response = asDict(err.response);
4222
+ for (const key of ["status", "statusCode"]) {
4223
+ const value = response?.[key];
4224
+ if (typeof value === "number" && Number.isFinite(value)) return value;
4225
+ }
4226
+ return null;
4227
+ }
4228
+ function isTerminalClientError(error) {
4229
+ const status = getHttpStatus(error);
4230
+ if (status === null) return false;
4231
+ return status >= 400 && status < 500 && status !== 429 && status !== 408;
4232
+ }
4233
+ var FAILURE_GROWTH_FACTOR = 2;
4234
+ var IDLE_GROWTH_FACTOR = 1.5;
4235
+ function nextFailureDelayMs(currentMs, maxMs) {
4236
+ return Math.min(Math.max(currentMs, 1) * FAILURE_GROWTH_FACTOR, maxMs);
4237
+ }
4238
+ function nextIdleDelayMs(currentMs, maxMs) {
4239
+ return Math.min(Math.ceil(Math.max(currentMs, 1) * IDLE_GROWTH_FACTOR), maxMs);
4240
+ }
4241
+ function applyJitter(delayMs, ratio, random = Math.random) {
4242
+ if (ratio <= 0) return delayMs;
4243
+ return Math.round(delayMs * (1 + random() * ratio));
4244
+ }
4245
+
4107
4246
  // src/streaming/stream-controller.ts
4108
4247
  var DEFAULT_STREAMING_CONFIG = {
4109
4248
  maxReconnectAttempts: 5,
4110
4249
  reconnectBaseDelayMs: 1e3,
4111
4250
  reconnectMaxDelayMs: 1e4,
4251
+ // NOTE (unchanged, deliberately): this watchdog force-disconnects a HEALTHY
4252
+ // SSE connection after 30s of silence and routes it through the same
4253
+ // handleConnectionError path as a real transport failure. Ordinary LLM
4254
+ // latency exceeds 30s routinely, so it fires against working streams — that
4255
+ // is what drives sessions into the polling fallback in the first place. The
4256
+ // fix is a server-side SSE heartbeat, not a bigger number here; raising this
4257
+ // is a guess against a moving target. Everything below only bounds what the
4258
+ // fallback costs once it has been entered.
4112
4259
  activityTimeoutMs: 3e4,
4113
- pollingIntervalMs: 2e3
4260
+ pollingIntervalMs: 5e3,
4261
+ pollingMaxIntervalMs: 3e4,
4262
+ pollingMaxConsecutiveFailures: 10,
4263
+ pollingMaxConsecutiveRateLimits: 3,
4264
+ pollingMaxConsecutiveClientErrors: 3,
4265
+ pollingMaxDurationMs: 18e5,
4266
+ pollingMaxRetryAfterMs: 3e5,
4267
+ pollingJitterRatio: 0.1,
4268
+ sseRetryIntervalMs: 6e4
4114
4269
  };
4115
4270
  var DEFAULT_POLLING_ENABLED = true;
4116
4271
  var ExecutionStreamControllerImpl = class {
@@ -4123,9 +4278,24 @@ var ExecutionStreamControllerImpl = class {
4123
4278
  seenEventIds = /* @__PURE__ */ new Set();
4124
4279
  _lastEventId = null;
4125
4280
  activityTimeoutId = null;
4126
- pollingIntervalId = null;
4127
4281
  finalExecution = null;
4128
4282
  finalError = null;
4283
+ // --- Fallback polling state ---
4284
+ // A self-rescheduling timeout, not setInterval: the delay has to change
4285
+ // between ticks (that is what backoff IS), and a timeout chain also cannot
4286
+ // stack overlapping requests the way a fixed interval does when the API is
4287
+ // slow to answer.
4288
+ pollTimeoutId = null;
4289
+ pollDelayMs = 0;
4290
+ pollConsecutiveFailures = 0;
4291
+ pollConsecutiveRateLimits = 0;
4292
+ pollConsecutiveClientErrors = 0;
4293
+ /** When the loop last observed anything new — the clock the give-up bound runs on. */
4294
+ pollLastProgressAt = 0;
4295
+ pollInFlight = false;
4296
+ lastPolledStatus = null;
4297
+ sseRetryTimeoutId = null;
4298
+ sseRetryInFlight = false;
4129
4299
  // Event callbacks
4130
4300
  listeners = {
4131
4301
  log: [],
@@ -4154,6 +4324,39 @@ var ExecutionStreamControllerImpl = class {
4154
4324
  this.config = { ...DEFAULT_STREAMING_CONFIG, ...options.config };
4155
4325
  this.pollingEnabled = options.pollingEnabled ?? DEFAULT_POLLING_ENABLED;
4156
4326
  }
4327
+ // Resolved fallback-poll settings. Read through `??` rather than off the
4328
+ // spread config so an explicit `undefined` in a caller's partial config still
4329
+ // lands on the default instead of NaN-ing the arithmetic.
4330
+ get pollBaseIntervalMs() {
4331
+ return this.config.pollingIntervalMs ?? DEFAULT_STREAMING_CONFIG.pollingIntervalMs;
4332
+ }
4333
+ get pollMaxIntervalMs() {
4334
+ return Math.max(
4335
+ this.config.pollingMaxIntervalMs ?? DEFAULT_STREAMING_CONFIG.pollingMaxIntervalMs,
4336
+ this.pollBaseIntervalMs
4337
+ );
4338
+ }
4339
+ get pollMaxConsecutiveFailures() {
4340
+ return this.config.pollingMaxConsecutiveFailures ?? DEFAULT_STREAMING_CONFIG.pollingMaxConsecutiveFailures;
4341
+ }
4342
+ get pollMaxConsecutiveRateLimits() {
4343
+ return this.config.pollingMaxConsecutiveRateLimits ?? DEFAULT_STREAMING_CONFIG.pollingMaxConsecutiveRateLimits;
4344
+ }
4345
+ get pollMaxConsecutiveClientErrors() {
4346
+ return this.config.pollingMaxConsecutiveClientErrors ?? DEFAULT_STREAMING_CONFIG.pollingMaxConsecutiveClientErrors;
4347
+ }
4348
+ get pollMaxDurationMs() {
4349
+ return this.config.pollingMaxDurationMs ?? DEFAULT_STREAMING_CONFIG.pollingMaxDurationMs;
4350
+ }
4351
+ get pollMaxRetryAfterMs() {
4352
+ return this.config.pollingMaxRetryAfterMs ?? DEFAULT_STREAMING_CONFIG.pollingMaxRetryAfterMs;
4353
+ }
4354
+ get pollJitterRatio() {
4355
+ return this.config.pollingJitterRatio ?? DEFAULT_STREAMING_CONFIG.pollingJitterRatio;
4356
+ }
4357
+ get sseRetryIntervalMs() {
4358
+ return this.config.sseRetryIntervalMs ?? DEFAULT_STREAMING_CONFIG.sseRetryIntervalMs;
4359
+ }
4157
4360
  /**
4158
4361
  * Get the last event ID (for resume support)
4159
4362
  */
@@ -4244,28 +4447,45 @@ var ExecutionStreamControllerImpl = class {
4244
4447
  console.log("[SDK:StreamController] executionId:", this.options.executionId);
4245
4448
  console.log("[SDK:StreamController] orgId:", this.options.orgId);
4246
4449
  console.log("[SDK:StreamController] apiKey:", authToken ? `${authToken.substring(0, 20)}...` : "MISSING");
4247
- this.sseClient = new SSEClient({
4450
+ if (this.sseClient) {
4451
+ this.sseClient.disconnect();
4452
+ this.sseClient = null;
4453
+ }
4454
+ const client = new SSEClient({
4248
4455
  url,
4249
4456
  authHeader: `Bearer ${authToken}`,
4250
4457
  orgId: this.options.orgId,
4251
4458
  fetch: this.options.fetch,
4252
- onEvent: (event) => this.handleEvent(event),
4459
+ // Resume from where this session left off. On a first connect this is
4460
+ // null and the header is omitted.
4461
+ lastEventId: this._lastEventId,
4462
+ onEvent: (event) => {
4463
+ if (this.sseClient !== client) return;
4464
+ this.handleEvent(event);
4465
+ },
4253
4466
  onError: (error) => {
4467
+ if (this.sseClient !== client) return;
4254
4468
  console.error("[SDK:StreamController] SSE error:", error);
4255
4469
  this.handleConnectionError(error);
4256
4470
  },
4257
4471
  onConnectionChange: (connected) => {
4472
+ if (this.sseClient !== client) return;
4258
4473
  console.log("[SDK:StreamController] SSE connection change:", connected);
4259
4474
  if (connected) {
4260
4475
  this.status = "connected";
4261
4476
  this.reconnectAttempts = 0;
4477
+ this.stopPolling();
4262
4478
  this.resetActivityTimeout();
4263
4479
  }
4264
- this.emit("connectionChange", { connected, usingFallback: false });
4480
+ this.emit("connectionChange", {
4481
+ connected,
4482
+ usingFallback: this.status === "fallback_polling"
4483
+ });
4265
4484
  }
4266
4485
  });
4486
+ this.sseClient = client;
4267
4487
  try {
4268
- await this.sseClient.connect();
4488
+ await client.connect();
4269
4489
  console.log("[SDK:StreamController] SSE connect() completed");
4270
4490
  } catch (error) {
4271
4491
  console.error("[SDK:StreamController] SSE connect() threw:", error);
@@ -4504,6 +4724,7 @@ var ExecutionStreamControllerImpl = class {
4504
4724
  handleComplete(data) {
4505
4725
  this.status = "closed";
4506
4726
  this.stopActivityTimeout();
4727
+ this.stopPolling();
4507
4728
  const sessionId = data.sessionId ?? this.options.sessionId;
4508
4729
  const execution = {
4509
4730
  uuid: this.options.executionId,
@@ -4530,6 +4751,7 @@ var ExecutionStreamControllerImpl = class {
4530
4751
  handleError(error) {
4531
4752
  this.status = "closed";
4532
4753
  this.stopActivityTimeout();
4754
+ this.stopPolling();
4533
4755
  this.finalError = error;
4534
4756
  this.emit("error", error);
4535
4757
  if (this.waitReject) {
@@ -4556,6 +4778,10 @@ var ExecutionStreamControllerImpl = class {
4556
4778
  this.handleError({ error: msg, errorCode: "stream_unrecoverable" });
4557
4779
  return;
4558
4780
  }
4781
+ if (this.status === "fallback_polling") {
4782
+ this.startPolling();
4783
+ return;
4784
+ }
4559
4785
  if (this.reconnectAttempts < this.config.maxReconnectAttempts) {
4560
4786
  this.status = "reconnecting";
4561
4787
  this.reconnectAttempts++;
@@ -4625,41 +4851,215 @@ var ExecutionStreamControllerImpl = class {
4625
4851
  }
4626
4852
  }
4627
4853
  /**
4628
- * Start polling fallback
4854
+ * Start polling fallback.
4855
+ *
4856
+ * Idempotent — calling it while a loop is already running is a no-op, so no
4857
+ * path can end up with two loops racing the same execution.
4629
4858
  */
4630
4859
  startPolling() {
4631
- this.pollingIntervalId = setInterval(async () => {
4632
- try {
4633
- const execution = await this.options.getExecution(this.options.executionId);
4634
- for (const log of execution.logs) {
4635
- if (!this.seenEventIds.has(log.uuid)) {
4636
- this.seenEventIds.add(log.uuid);
4637
- this.emit("log", log);
4638
- }
4639
- }
4640
- if (execution.status === "completed") {
4641
- this.stopPolling();
4642
- this.handleComplete(execution);
4643
- } else if (execution.status === "failed" || execution.status === "cancelled") {
4644
- this.stopPolling();
4645
- this.handleError({
4646
- error: execution.error ?? `Execution ${execution.status}`,
4647
- errorCode: execution.status
4648
- });
4860
+ if (this.pollTimeoutId !== null || this.pollInFlight) {
4861
+ return;
4862
+ }
4863
+ this.pollLastProgressAt = Date.now();
4864
+ this.pollDelayMs = this.pollBaseIntervalMs;
4865
+ this.pollConsecutiveFailures = 0;
4866
+ this.pollConsecutiveRateLimits = 0;
4867
+ this.pollConsecutiveClientErrors = 0;
4868
+ this.lastPolledStatus = null;
4869
+ this.stopActivityTimeout();
4870
+ this.scheduleSSERetry();
4871
+ this.schedulePoll(this.pollDelayMs);
4872
+ }
4873
+ /**
4874
+ * Queue the next poll tick.
4875
+ */
4876
+ schedulePoll(delayMs) {
4877
+ if (this.status !== "fallback_polling") {
4878
+ return;
4879
+ }
4880
+ this.pollTimeoutId = setTimeout(() => {
4881
+ this.pollTimeoutId = null;
4882
+ void this.runPoll();
4883
+ }, Math.max(0, Math.round(delayMs)));
4884
+ }
4885
+ /**
4886
+ * One poll tick: fetch state, emit what is new, then decide how long to wait
4887
+ * before the next one — or whether there should be a next one at all.
4888
+ */
4889
+ async runPoll() {
4890
+ if (this.status !== "fallback_polling") {
4891
+ return;
4892
+ }
4893
+ if (Date.now() - this.pollLastProgressAt >= this.pollMaxDurationMs) {
4894
+ this.failPolling(
4895
+ `Streaming fallback gave up: no execution progress for ${Math.round(this.pollMaxDurationMs / 1e3)}s`,
4896
+ "polling_timeout"
4897
+ );
4898
+ return;
4899
+ }
4900
+ this.pollInFlight = true;
4901
+ try {
4902
+ const execution = await this.options.getExecution(this.options.executionId);
4903
+ this.pollConsecutiveFailures = 0;
4904
+ this.pollConsecutiveRateLimits = 0;
4905
+ this.pollConsecutiveClientErrors = 0;
4906
+ let sawNewInfo = false;
4907
+ for (const log of execution.logs ?? []) {
4908
+ if (!this.seenEventIds.has(log.uuid)) {
4909
+ this.seenEventIds.add(log.uuid);
4910
+ this.emit("log", log);
4911
+ sawNewInfo = true;
4649
4912
  }
4650
- } catch (error) {
4651
- console.error("Polling error:", error);
4652
4913
  }
4653
- }, this.config.pollingIntervalMs);
4914
+ if (execution.status !== this.lastPolledStatus) {
4915
+ this.lastPolledStatus = execution.status;
4916
+ sawNewInfo = true;
4917
+ }
4918
+ if (sawNewInfo) {
4919
+ this.pollLastProgressAt = Date.now();
4920
+ }
4921
+ if (execution.status === "completed") {
4922
+ this.stopPolling();
4923
+ this.handleComplete(execution);
4924
+ return;
4925
+ }
4926
+ if (execution.status === "failed" || execution.status === "cancelled") {
4927
+ this.stopPolling();
4928
+ this.handleError({
4929
+ error: execution.error ?? `Execution ${execution.status}`,
4930
+ errorCode: execution.status
4931
+ });
4932
+ return;
4933
+ }
4934
+ this.pollDelayMs = sawNewInfo ? this.pollBaseIntervalMs : nextIdleDelayMs(this.pollDelayMs, this.pollMaxIntervalMs);
4935
+ } catch (error) {
4936
+ if (!this.handlePollError(error)) {
4937
+ return;
4938
+ }
4939
+ } finally {
4940
+ this.pollInFlight = false;
4941
+ }
4942
+ this.schedulePoll(applyJitter(this.pollDelayMs, this.pollJitterRatio));
4943
+ }
4944
+ /**
4945
+ * Apply backpressure for a failed poll.
4946
+ *
4947
+ * @returns `true` to keep polling (with `pollDelayMs` updated), `false` when
4948
+ * the loop has been terminated.
4949
+ */
4950
+ handlePollError(error) {
4951
+ const retryAfterMs = getRetryAfterMs(error);
4952
+ const rateLimited = isRateLimited(error) || retryAfterMs !== null;
4953
+ if (rateLimited) {
4954
+ this.pollConsecutiveRateLimits++;
4955
+ console.warn(
4956
+ "[SDK:StreamController] Poll rate-limited",
4957
+ retryAfterMs !== null ? `(server asked for ${Math.round(retryAfterMs / 1e3)}s)` : "(no reset named)"
4958
+ );
4959
+ if (retryAfterMs !== null && retryAfterMs > this.pollMaxRetryAfterMs) {
4960
+ this.failPolling(
4961
+ `Rate limited: the API asked for ${Math.round(retryAfterMs / 1e3)}s, longer than this stream will wait`,
4962
+ "rate_limited"
4963
+ );
4964
+ return false;
4965
+ }
4966
+ if (this.pollConsecutiveRateLimits >= this.pollMaxConsecutiveRateLimits) {
4967
+ this.failPolling(
4968
+ "Rate limited by the API; stopped the streaming fallback rather than keep spending the key quota",
4969
+ "rate_limited"
4970
+ );
4971
+ return false;
4972
+ }
4973
+ this.pollDelayMs = Math.max(retryAfterMs ?? UNNAMED_RATE_LIMIT_WAIT_MS, this.pollDelayMs);
4974
+ return true;
4975
+ }
4976
+ if (isTerminalClientError(error)) {
4977
+ this.pollConsecutiveClientErrors++;
4978
+ const status = getHttpStatus(error);
4979
+ console.warn(
4980
+ `[SDK:StreamController] Poll got HTTP ${status} (${this.pollConsecutiveClientErrors}/${this.pollMaxConsecutiveClientErrors})`
4981
+ );
4982
+ if (this.pollConsecutiveClientErrors >= this.pollMaxConsecutiveClientErrors) {
4983
+ this.failPolling(
4984
+ `Execution status endpoint returned HTTP ${status} ${this.pollConsecutiveClientErrors} times \u2014 cannot track this run`,
4985
+ "polling_unrecoverable"
4986
+ );
4987
+ return false;
4988
+ }
4989
+ this.pollDelayMs = nextFailureDelayMs(this.pollDelayMs, this.pollMaxIntervalMs);
4990
+ return true;
4991
+ }
4992
+ this.pollConsecutiveFailures++;
4993
+ console.warn(
4994
+ `[SDK:StreamController] Poll failed (${this.pollConsecutiveFailures}/${this.pollMaxConsecutiveFailures}):`,
4995
+ error
4996
+ );
4997
+ if (this.pollConsecutiveFailures >= this.pollMaxConsecutiveFailures) {
4998
+ this.failPolling(
4999
+ `Streaming fallback failed ${this.pollConsecutiveFailures} times in a row`,
5000
+ "polling_failed"
5001
+ );
5002
+ return false;
5003
+ }
5004
+ this.pollDelayMs = nextFailureDelayMs(this.pollDelayMs, this.pollMaxIntervalMs);
5005
+ return true;
5006
+ }
5007
+ /**
5008
+ * Terminate the fallback loop and surface the reason.
5009
+ */
5010
+ failPolling(error, errorCode) {
5011
+ this.stopPolling();
5012
+ this.handleError({ error, errorCode });
4654
5013
  }
4655
5014
  /**
4656
- * Stop polling
5015
+ * Stop polling, including the periodic SSE re-attempt that belongs to it.
4657
5016
  */
4658
5017
  stopPolling() {
4659
- if (this.pollingIntervalId) {
4660
- clearInterval(this.pollingIntervalId);
4661
- this.pollingIntervalId = null;
5018
+ if (this.pollTimeoutId !== null) {
5019
+ clearTimeout(this.pollTimeoutId);
5020
+ this.pollTimeoutId = null;
5021
+ }
5022
+ if (this.sseRetryTimeoutId !== null) {
5023
+ clearTimeout(this.sseRetryTimeoutId);
5024
+ this.sseRetryTimeoutId = null;
5025
+ }
5026
+ }
5027
+ /**
5028
+ * Queue the next attempt to get the real stream back.
5029
+ */
5030
+ scheduleSSERetry() {
5031
+ if (this.sseRetryTimeoutId !== null || this.status !== "fallback_polling") {
5032
+ return;
4662
5033
  }
5034
+ this.sseRetryTimeoutId = setTimeout(() => {
5035
+ this.sseRetryTimeoutId = null;
5036
+ this.attemptSSERecovery();
5037
+ }, this.sseRetryIntervalMs);
5038
+ }
5039
+ /**
5040
+ * Re-attempt SSE from inside the fallback, so a transient outage costs the
5041
+ * session a few polls rather than its stream for the lifetime of the tab.
5042
+ *
5043
+ * Success is NOT observed here: `SSEClient.connect()` resolves when the
5044
+ * stream ENDS, not when it opens. The open is observed by
5045
+ * `onConnectionChange(true)`, which flips status and stops this loop. The
5046
+ * promise below only tells us the attempt is over so the next one can be
5047
+ * queued.
5048
+ */
5049
+ attemptSSERecovery() {
5050
+ if (this.status !== "fallback_polling") {
5051
+ return;
5052
+ }
5053
+ if (this.sseRetryInFlight) {
5054
+ this.scheduleSSERetry();
5055
+ return;
5056
+ }
5057
+ this.sseRetryInFlight = true;
5058
+ void this.connectSSE().catch(() => {
5059
+ }).finally(() => {
5060
+ this.sseRetryInFlight = false;
5061
+ this.scheduleSSERetry();
5062
+ });
4663
5063
  }
4664
5064
  /**
4665
5065
  * Reset activity timeout
@@ -4791,12 +5191,23 @@ async function collectImagesFromStream(stream, options = {}) {
4791
5191
  });
4792
5192
  }
4793
5193
 
5194
+ // src/utils/api-paths.ts
5195
+ var API_V1 = "/api/v1";
5196
+ var EXECUTION_PATH = `${API_V1}/executions`;
5197
+ function executionPath(executionId) {
5198
+ return `${EXECUTION_PATH}/${executionId}`;
5199
+ }
5200
+ function executionCancelPath(executionId) {
5201
+ return `${EXECUTION_PATH}/${executionId}/cancel`;
5202
+ }
5203
+
4794
5204
  // src/resources/agents.ts
4795
5205
  var AgentsAPI = class extends BaseResourceAPI {
4796
5206
  constructor(http, getStreamConfig) {
4797
5207
  super(http, "/api/v1/agents");
4798
5208
  this.getStreamConfig = getStreamConfig;
4799
5209
  }
5210
+ getStreamConfig;
4800
5211
  // ===== LIST Operations =====
4801
5212
  /**
4802
5213
  * List all agents in the organization
@@ -5053,7 +5464,7 @@ var AgentsAPI = class extends BaseResourceAPI {
5053
5464
  * ```
5054
5465
  */
5055
5466
  async getExecution(executionId) {
5056
- return this.http.get(`/executions/${executionId}`);
5467
+ return this.http.get(executionPath(executionId));
5057
5468
  }
5058
5469
  // ===== EDIT Operations =====
5059
5470
  /**
@@ -5108,7 +5519,7 @@ var AgentsAPI = class extends BaseResourceAPI {
5108
5519
  */
5109
5520
  async cancelExecution(executionId) {
5110
5521
  return this.http.post(
5111
- `/executions/${executionId}/cancel`,
5522
+ executionCancelPath(executionId),
5112
5523
  void 0,
5113
5524
  { skipRetry: true }
5114
5525
  );
@@ -5609,6 +6020,7 @@ var ArtifactsAPI = class extends BaseResourceAPI {
5609
6020
  }
5610
6021
  return this.http.request({
5611
6022
  method: "POST",
6023
+ // KNOWN BROKEN — same missing `/api/v1` prefix as listArtifacts above.
5612
6024
  path: `/executions/${executionId}/artifacts`,
5613
6025
  body: formData,
5614
6026
  headers: {
@@ -5702,6 +6114,7 @@ var SessionsAPI = class extends BaseResourceAPI {
5702
6114
  super(http, "/api/v1/sessions");
5703
6115
  this.getStreamConfig = getStreamConfig;
5704
6116
  }
6117
+ getStreamConfig;
5705
6118
  // ===== LIST Operations =====
5706
6119
  /**
5707
6120
  * List all sessions in the organization
@@ -5881,7 +6294,7 @@ var SessionsAPI = class extends BaseResourceAPI {
5881
6294
  * Get execution result
5882
6295
  */
5883
6296
  async getExecution(executionId) {
5884
- return this.http.get(`/executions/${executionId}`);
6297
+ return this.http.get(executionPath(executionId));
5885
6298
  }
5886
6299
  // ===== EDIT Operations =====
5887
6300
  /**
@@ -6021,6 +6434,7 @@ var KanbanAPI = class extends BaseResourceAPI {
6021
6434
  super(http, "/api/v1/kanban");
6022
6435
  this.getStreamConfig = getStreamConfig;
6023
6436
  }
6437
+ getStreamConfig;
6024
6438
  // ===== BOARD =====
6025
6439
  /**
6026
6440
  * Get the current org's board (lanes in declared order). Auto-provisions
@@ -6155,7 +6569,7 @@ var KanbanAPI = class extends BaseResourceAPI {
6155
6569
  * @returns Execution details.
6156
6570
  */
6157
6571
  async getCardRun(executionId) {
6158
- return this.http.get(`/api/v1/agents/executions/${executionId}`);
6572
+ return this.http.get(executionPath(executionId));
6159
6573
  }
6160
6574
  /**
6161
6575
  * Create a stream controller for a card's run, keyed on the card's
@@ -6222,6 +6636,7 @@ var RemoteAgentsAPI = class extends BaseResourceAPI {
6222
6636
  super(http, "/api/v1/remote-agents");
6223
6637
  this.getStreamConfig = getStreamConfig;
6224
6638
  }
6639
+ getStreamConfig;
6225
6640
  /**
6226
6641
  * List all remote agents in the organization
6227
6642
  *
@@ -6355,7 +6770,7 @@ var RemoteAgentsAPI = class extends BaseResourceAPI {
6355
6770
  apiKey: config.apiKey,
6356
6771
  tokenProvider: config.tokenProvider,
6357
6772
  orgId: config.orgId,
6358
- getExecution: (id) => this.http.get(`/executions/${id}`)
6773
+ getExecution: (id) => this.http.get(executionPath(id))
6359
6774
  });
6360
6775
  return controller;
6361
6776
  }