@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.
@@ -258,12 +258,9 @@ function useKanbanBoard(options) {
258
258
  ]);
259
259
  setBoard(nextBoard);
260
260
  setCards(cardList.cards);
261
- setSelectedCard((prev) => {
262
- if (!prev) return prev;
263
- const next = cardList.cards.find((c) => c.id === prev.id);
264
- if (!next) return prev;
265
- return next.run ? next : { ...next, run: prev.run };
266
- });
261
+ setSelectedCard(
262
+ (prev) => prev ? cardList.cards.find((c) => c.id === prev.id) ?? prev : prev
263
+ );
267
264
  } catch (err) {
268
265
  fail(err, "Failed to load board");
269
266
  } finally {
@@ -278,12 +275,9 @@ function useKanbanBoard(options) {
278
275
  ]);
279
276
  setBoard(nextBoard);
280
277
  setCards(cardList.cards);
281
- setSelectedCard((prev) => {
282
- if (!prev) return prev;
283
- const next = cardList.cards.find((c) => c.id === prev.id);
284
- if (!next) return prev;
285
- return next.run ? next : { ...next, run: prev.run };
286
- });
278
+ setSelectedCard(
279
+ (prev) => prev ? cardList.cards.find((c) => c.id === prev.id) ?? prev : prev
280
+ );
287
281
  } catch {
288
282
  }
289
283
  }, [adapter]);
@@ -304,7 +298,7 @@ function useKanbanBoard(options) {
304
298
  try {
305
299
  const full = await adapter.getCard(cardId);
306
300
  setSelectedCard(
307
- (prev) => prev == null || prev.id === cardId ? full : prev
301
+ (prev) => prev && prev.id === cardId ? full : prev
308
302
  );
309
303
  } catch (err) {
310
304
  if (!known) fail(err, "Failed to open card");
@@ -413,27 +407,6 @@ function useKanbanBoard(options) {
413
407
  },
414
408
  [refresh]
415
409
  );
416
- const deleteCard = useCallback(
417
- async (cardId) => {
418
- if (!adapter.deleteCard) return false;
419
- setError(null);
420
- const prevCards = cards;
421
- const prevSelected = selectedCard;
422
- setCards((prev) => prev.filter((c) => c.id !== cardId));
423
- setSelectedCard((prev) => prev && prev.id === cardId ? null : prev);
424
- try {
425
- await adapter.deleteCard(cardId);
426
- void silentRefresh();
427
- return true;
428
- } catch (err) {
429
- setCards(prevCards);
430
- setSelectedCard(prevSelected);
431
- fail(err, "Failed to delete card");
432
- return false;
433
- }
434
- },
435
- [adapter, cards, selectedCard, silentRefresh, fail]
436
- );
437
410
  return {
438
411
  board,
439
412
  lanes: board?.lanes ?? [],
@@ -449,7 +422,6 @@ function useKanbanBoard(options) {
449
422
  createCard,
450
423
  updateCard,
451
424
  transitionCard,
452
- deleteCard,
453
425
  handleRunTerminal
454
426
  };
455
427
  }
@@ -2743,9 +2715,6 @@ function KanbanCard({
2743
2715
  onClick,
2744
2716
  selected,
2745
2717
  dueState,
2746
- draggable,
2747
- onDragStart,
2748
- onDragEnd,
2749
2718
  className
2750
2719
  }) {
2751
2720
  const runStatus = card.run?.status;
@@ -2774,20 +2743,10 @@ function KanbanCard({
2774
2743
  onClick: () => onClick?.(card),
2775
2744
  "aria-pressed": selected,
2776
2745
  "aria-busy": isWorking,
2777
- draggable,
2778
- onDragStart: draggable ? (e) => {
2779
- e.dataTransfer.effectAllowed = "move";
2780
- e.dataTransfer.setData("text/plain", card.id);
2781
- onDragStart?.(card);
2782
- } : void 0,
2783
- onDragEnd: draggable ? () => onDragEnd?.() : void 0,
2784
2746
  className: cn9(
2785
- // Solid card: fill + border + a faint token top-sheen (adapts to theme),
2786
- // matching the host's cockpit cards (work-shell v2 visual pass).
2787
- "relative w-full text-left rounded-xl border bg-card bg-gradient-to-b from-foreground/[0.03] to-transparent p-3 shadow-sm motion-safe:transition-shadow",
2747
+ "relative w-full text-left rounded-lg border bg-card p-3 shadow-sm transition",
2788
2748
  "hover:shadow-md hover:border-primary/40",
2789
2749
  "focus:outline-none focus:ring-2 focus:ring-ring/50",
2790
- draggable && "cursor-grab active:cursor-grabbing",
2791
2750
  // The working card carries the SINGLE focal motion on the board (F11).
2792
2751
  isWorking ? "border-info/60 ring-2 ring-info/40 animate-pulse" : selected ? "border-primary/60 ring-2 ring-primary/30" : "border-border",
2793
2752
  className
@@ -2864,11 +2823,6 @@ function CapMeter({ count, cap, className }) {
2864
2823
  }
2865
2824
  );
2866
2825
  }
2867
- var AGENT_GROUPS = [
2868
- { kind: "cadra", label: "Cadra agents" },
2869
- { kind: "remote", label: "Your own agents" },
2870
- { kind: "human", label: "Teammates" }
2871
- ];
2872
2826
  function AgentKindIcon({ kind }) {
2873
2827
  const common = {
2874
2828
  viewBox: "0 0 24 24",
@@ -2911,55 +2865,17 @@ function OnlineDot({ online }) {
2911
2865
  }
2912
2866
  );
2913
2867
  }
2914
- function RemoteBadge() {
2915
- return /* @__PURE__ */ jsx("span", { className: "shrink-0 rounded-full border border-accent/40 bg-accent/15 px-1.5 py-px text-[10px] font-semibold leading-4 text-accent", children: "Remote" });
2916
- }
2917
2868
  function LaneAgentPicker({
2918
2869
  selected,
2919
2870
  options,
2920
2871
  onSelect,
2921
2872
  canEdit = true,
2922
2873
  lockedReason,
2923
- onEditAgent,
2924
2874
  className
2925
2875
  }) {
2926
2876
  const [open, setOpen] = useState(false);
2927
- const [placement, setPlacement] = useState(null);
2928
2877
  const rootRef = useRef(null);
2929
- const triggerRef = useRef(null);
2930
2878
  const editable = canEdit && !!onSelect && (options?.length ?? 0) > 0;
2931
- const openMenu = () => {
2932
- const el = triggerRef.current;
2933
- if (el && typeof window !== "undefined") {
2934
- const rect = el.getBoundingClientRect();
2935
- const vw = window.innerWidth || 0;
2936
- const vh = window.innerHeight || 0;
2937
- const margin = 8;
2938
- const gap = 4;
2939
- const desired = 288;
2940
- const width = Math.max(
2941
- Math.min(rect.width || 224, Math.max(vw - margin * 2, 160)),
2942
- 160
2943
- );
2944
- const left = Math.min(
2945
- Math.max(rect.left, margin),
2946
- Math.max(vw - width - margin, margin)
2947
- );
2948
- const spaceBelow = vh - rect.bottom - gap - margin;
2949
- const spaceAbove = rect.top - gap - margin;
2950
- const flipUp = spaceBelow < desired && spaceAbove > spaceBelow;
2951
- const maxHeight = Math.max(
2952
- Math.min(desired, flipUp ? spaceAbove : spaceBelow),
2953
- 120
2954
- );
2955
- setPlacement(
2956
- flipUp ? { bottom: Math.max(vh - rect.top + gap, margin), left, width, maxHeight } : { top: rect.bottom + gap, left, width, maxHeight }
2957
- );
2958
- } else {
2959
- setPlacement(null);
2960
- }
2961
- setOpen(true);
2962
- };
2963
2879
  useEffect(() => {
2964
2880
  if (!open) return;
2965
2881
  const onDocDown = (e) => {
@@ -2993,28 +2909,14 @@ function LaneAgentPicker({
2993
2909
  }
2994
2910
  ),
2995
2911
  /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: displayLabel }),
2996
- kind === "remote" && /* @__PURE__ */ jsx(RemoteBadge, {}),
2997
2912
  /* @__PURE__ */ jsx(OnlineDot, { online: selected?.online })
2998
2913
  ] });
2999
- const grouped = AGENT_GROUPS.map((group) => ({
3000
- ...group,
3001
- items: (options ?? []).filter((opt) => (opt.kind ?? "cadra") === group.kind)
3002
- })).filter((group) => group.items.length > 0);
3003
- const menuStyle = placement ? {
3004
- position: "fixed",
3005
- top: placement.top,
3006
- bottom: placement.bottom,
3007
- left: placement.left,
3008
- width: placement.width,
3009
- maxHeight: placement.maxHeight
3010
- } : void 0;
3011
2914
  return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: cn9("relative mt-2 w-full", className), children: [
3012
2915
  editable ? /* @__PURE__ */ jsxs(
3013
2916
  "button",
3014
2917
  {
3015
- ref: triggerRef,
3016
2918
  type: "button",
3017
- onClick: () => open ? setOpen(false) : openMenu(),
2919
+ onClick: () => setOpen((o) => !o),
3018
2920
  "aria-haspopup": "listbox",
3019
2921
  "aria-expanded": open,
3020
2922
  "aria-label": `Assigned agent: ${displayLabel}. Change`,
@@ -3092,106 +2994,47 @@ function LaneAgentPicker({
3092
2994
  ),
3093
2995
  /* @__PURE__ */ jsx("span", { children: lockedReason })
3094
2996
  ] }),
3095
- editable && open && /* @__PURE__ */ jsxs(
3096
- "div",
2997
+ editable && open && /* @__PURE__ */ jsx(
2998
+ "ul",
3097
2999
  {
3098
- style: menuStyle,
3000
+ role: "listbox",
3001
+ "aria-label": "Assign an agent",
3099
3002
  className: cn9(
3100
- "flex flex-col overflow-hidden rounded-lg border border-border bg-popover shadow-lg",
3101
- menuStyle ? "z-50" : "absolute z-20 mt-1 max-h-64 w-full"
3003
+ "absolute z-20 mt-1 max-h-64 w-full overflow-y-auto rounded-lg border border-border",
3004
+ "bg-popover p-1 shadow-lg"
3102
3005
  ),
3103
- children: [
3104
- /* @__PURE__ */ jsx(
3105
- "ul",
3106
- {
3107
- role: "listbox",
3108
- "aria-label": "Assign an agent",
3109
- className: "min-h-0 flex-1 overflow-y-auto p-1",
3110
- children: grouped.map((group) => /* @__PURE__ */ jsxs("li", { role: "presentation", children: [
3111
- /* @__PURE__ */ jsx("div", { className: "px-2 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground", children: group.label }),
3112
- /* @__PURE__ */ jsx("ul", { role: "group", "aria-label": group.label, children: group.items.map((opt) => {
3113
- const isSelected = opt.id === selected?.id;
3114
- const optKind = opt.kind ?? "cadra";
3115
- return /* @__PURE__ */ jsx("li", { role: "option", "aria-selected": isSelected, children: /* @__PURE__ */ jsxs(
3116
- "button",
3117
- {
3118
- type: "button",
3119
- onClick: () => {
3120
- onSelect(opt.id);
3121
- setOpen(false);
3122
- },
3123
- className: cn9(
3124
- "flex min-h-11 w-full items-center gap-1.5 rounded-md px-2 text-left text-xs font-medium",
3125
- "hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring/50",
3126
- isSelected ? "text-primary" : "text-foreground"
3127
- ),
3128
- children: [
3129
- /* @__PURE__ */ jsx(
3130
- "span",
3131
- {
3132
- className: cn9(
3133
- "grid h-5 w-5 shrink-0 place-items-center rounded-full",
3134
- optKind === "cadra" ? "bg-primary/15 text-primary" : "bg-secondary text-foreground"
3135
- ),
3136
- children: /* @__PURE__ */ jsx(AgentKindIcon, { kind: optKind })
3137
- }
3138
- ),
3139
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: opt.label }),
3140
- optKind === "remote" && /* @__PURE__ */ jsx(RemoteBadge, {}),
3141
- /* @__PURE__ */ jsx(OnlineDot, { online: opt.online }),
3142
- isSelected && /* @__PURE__ */ jsx(
3143
- "svg",
3144
- {
3145
- viewBox: "0 0 24 24",
3146
- fill: "none",
3147
- stroke: "currentColor",
3148
- strokeWidth: 2,
3149
- strokeLinecap: "round",
3150
- strokeLinejoin: "round",
3151
- "aria-hidden": true,
3152
- className: "h-3.5 w-3.5 shrink-0",
3153
- children: /* @__PURE__ */ jsx("path", { d: "M20 6L9 17l-5-5" })
3154
- }
3155
- )
3156
- ]
3157
- }
3158
- ) }, opt.id);
3159
- }) })
3160
- ] }, group.kind))
3161
- }
3162
- ),
3163
- onEditAgent && selected && /* @__PURE__ */ jsx("div", { className: "border-t border-border p-1", children: /* @__PURE__ */ jsxs(
3006
+ children: options.map((opt) => {
3007
+ const isSelected = opt.id === selected?.id;
3008
+ return /* @__PURE__ */ jsx("li", { role: "option", "aria-selected": isSelected, children: /* @__PURE__ */ jsxs(
3164
3009
  "button",
3165
3010
  {
3166
3011
  type: "button",
3167
3012
  onClick: () => {
3168
- onEditAgent(selected.id);
3013
+ onSelect(opt.id);
3169
3014
  setOpen(false);
3170
3015
  },
3171
3016
  className: cn9(
3172
- "flex min-h-11 w-full items-center gap-1.5 rounded-md px-2 text-left text-xs font-medium text-foreground",
3173
- "hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring/50"
3017
+ "flex min-h-11 w-full items-center gap-1.5 rounded-md px-2 text-left text-xs font-medium",
3018
+ "hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring/50",
3019
+ isSelected ? "text-primary" : "text-foreground"
3174
3020
  ),
3175
3021
  children: [
3176
3022
  /* @__PURE__ */ jsx(
3177
- "svg",
3023
+ "span",
3178
3024
  {
3179
- viewBox: "0 0 24 24",
3180
- fill: "none",
3181
- stroke: "currentColor",
3182
- strokeWidth: 2,
3183
- strokeLinecap: "round",
3184
- strokeLinejoin: "round",
3185
- "aria-hidden": true,
3186
- className: "h-3.5 w-3.5 shrink-0 text-muted-foreground",
3187
- children: /* @__PURE__ */ jsx("path", { d: "M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" })
3025
+ className: cn9(
3026
+ "grid h-5 w-5 shrink-0 place-items-center rounded-full",
3027
+ (opt.kind ?? "cadra") === "cadra" ? "bg-primary/15 text-primary" : "bg-secondary text-foreground"
3028
+ ),
3029
+ children: /* @__PURE__ */ jsx(AgentKindIcon, { kind: opt.kind ?? "cadra" })
3188
3030
  }
3189
3031
  ),
3190
- "Edit this agent"
3032
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: opt.label }),
3033
+ /* @__PURE__ */ jsx(OnlineDot, { online: opt.online })
3191
3034
  ]
3192
3035
  }
3193
- ) })
3194
- ]
3036
+ ) }, opt.id);
3037
+ })
3195
3038
  }
3196
3039
  )
3197
3040
  ] });
@@ -3216,12 +3059,6 @@ function KanbanColumn({
3216
3059
  count,
3217
3060
  agentPicker,
3218
3061
  canEditHandlers,
3219
- draggableCards,
3220
- dragActive,
3221
- dropAllowed,
3222
- onCardDragStart,
3223
- onCardDragEnd,
3224
- onLaneDrop,
3225
3062
  className
3226
3063
  }) {
3227
3064
  const headerCount = count ?? cards.length;
@@ -3232,21 +3069,9 @@ function KanbanColumn({
3232
3069
  return /* @__PURE__ */ jsxs(
3233
3070
  "div",
3234
3071
  {
3235
- onDragOver: dropAllowed ? (e) => {
3236
- e.preventDefault();
3237
- e.dataTransfer.dropEffect = "move";
3238
- } : void 0,
3239
- onDrop: dropAllowed && onLaneDrop ? (e) => {
3240
- e.preventDefault();
3241
- onLaneDrop(lane);
3242
- } : void 0,
3243
3072
  className: cn9(
3244
- "flex w-[85vw] max-w-sm snap-start shrink-0 flex-col rounded-xl bg-muted/40 border border-border transition sm:w-72 sm:max-w-none",
3073
+ "flex w-[85vw] max-w-sm snap-start shrink-0 flex-col rounded-xl bg-muted/40 border border-border sm:w-72 sm:max-w-none",
3245
3074
  isScheduled && "border-border",
3246
- // MV4 drag affordance: allowed targets glow; other lanes dim so the
3247
- // valid drop zones read at a glance.
3248
- dragActive && dropAllowed && "border-primary/60 ring-2 ring-primary/40 bg-primary/5",
3249
- dragActive && !dropAllowed && "opacity-55",
3250
3075
  className
3251
3076
  ),
3252
3077
  children: [
@@ -3315,10 +3140,7 @@ function KanbanColumn({
3315
3140
  card,
3316
3141
  onClick: onCardClick,
3317
3142
  selected: card.id === selectedCardId,
3318
- dueState: dueStateFor(card, lane, now),
3319
- draggable: draggableCards,
3320
- onDragStart: onCardDragStart,
3321
- onDragEnd: onCardDragEnd
3143
+ dueState: dueStateFor(card, lane, now)
3322
3144
  },
3323
3145
  card.id
3324
3146
  )),
@@ -3478,8 +3300,6 @@ function KanbanBoard({
3478
3300
  laneAgentPicker,
3479
3301
  scheduleStatus,
3480
3302
  focusCardId,
3481
- renderCardDrawer,
3482
- enableDrag,
3483
3303
  className
3484
3304
  }) {
3485
3305
  const {
@@ -3494,7 +3314,6 @@ function KanbanBoard({
3494
3314
  createCard,
3495
3315
  updateCard,
3496
3316
  transitionCard,
3497
- deleteCard,
3498
3317
  handleRunTerminal
3499
3318
  } = useKanbanBoard({ adapter, onError, onCardCreated, pollIntervalMs });
3500
3319
  const lastFocusedRef = useRef(void 0);
@@ -3539,44 +3358,6 @@ function KanbanBoard({
3539
3358
  (cardId, toLane) => transitionCard({ cardId, targetLane: toLane }),
3540
3359
  [transitionCard]
3541
3360
  );
3542
- const [dragging, setDragging] = useState(null);
3543
- const draggingRef = useRef(null);
3544
- const dropInFlightRef = useRef(null);
3545
- const allowedDropLanes = useMemo(() => {
3546
- if (!dragging || !board?.transitions) return /* @__PURE__ */ new Set();
3547
- return new Set(
3548
- board.transitions.filter((t) => t.fromLane === dragging.lane && t.manualAllowed).map((t) => t.toLane)
3549
- );
3550
- }, [dragging, board?.transitions]);
3551
- const handleCardDragStart = useCallback((card) => {
3552
- const d = { id: card.id, lane: card.lane };
3553
- draggingRef.current = d;
3554
- setDragging(d);
3555
- }, []);
3556
- const handleCardDragEnd = useCallback(() => {
3557
- draggingRef.current = null;
3558
- setDragging(null);
3559
- }, []);
3560
- const handleLaneDrop = useCallback(
3561
- (lane) => {
3562
- const cur = draggingRef.current;
3563
- draggingRef.current = null;
3564
- setDragging(null);
3565
- if (!cur || lane.key === cur.lane) return;
3566
- if (dropInFlightRef.current === cur.id) return;
3567
- const allowed = (board?.transitions ?? []).some(
3568
- (t) => t.fromLane === cur.lane && t.toLane === lane.key && t.manualAllowed
3569
- );
3570
- if (!allowed) return;
3571
- dropInFlightRef.current = cur.id;
3572
- void Promise.resolve(transitionCard({ cardId: cur.id, targetLane: lane.key })).finally(
3573
- () => {
3574
- if (dropInFlightRef.current === cur.id) dropInFlightRef.current = null;
3575
- }
3576
- );
3577
- },
3578
- [board?.transitions, transitionCard]
3579
- );
3580
3361
  const initialLaneKey = lanes.find((l) => !l.terminal)?.key ?? lanes[0]?.key;
3581
3362
  const selectedLane = selectedCard ? lanes.find((l) => l.key === selectedCard.lane) : void 0;
3582
3363
  const canAdvanceSelected = !!selectedLane && !selectedLane.terminal;
@@ -3597,32 +3378,12 @@ function KanbanBoard({
3597
3378
  showCreate: enableCreate && lane.key === initialLaneKey,
3598
3379
  count: laneCounts?.[lane.key],
3599
3380
  agentPicker: laneAgentPicker?.(lane),
3600
- canEditHandlers,
3601
- draggableCards: enableDrag && enableAdvance,
3602
- dragActive: !!dragging,
3603
- dropAllowed: !!dragging && dragging.lane !== lane.key && allowedDropLanes.has(lane.key),
3604
- onCardDragStart: handleCardDragStart,
3605
- onCardDragEnd: handleCardDragEnd,
3606
- onLaneDrop: handleLaneDrop
3381
+ canEditHandlers
3607
3382
  },
3608
3383
  lane.id ?? lane.key
3609
3384
  ))
3610
3385
  ] }),
3611
- selectedCard && (renderCardDrawer ? renderCardDrawer({
3612
- card: selectedCard,
3613
- adapter,
3614
- lanes,
3615
- transitions: enableAdvance ? board?.transitions : void 0,
3616
- canAdvance: canAdvanceSelected,
3617
- onClose: closeCard,
3618
- onRetry: onRetryCard ? handleRetry : void 0,
3619
- onUpdate: enableEdit ? handleUpdate : void 0,
3620
- onAdvance: enableAdvance ? handleAdvance : void 0,
3621
- onTransition: enableAdvance ? handleTransition : void 0,
3622
- onDelete: adapter.deleteCard ? deleteCard : void 0,
3623
- onRunTerminal: handleRunTerminal,
3624
- onError
3625
- }) : /* @__PURE__ */ jsx(
3386
+ selectedCard && /* @__PURE__ */ jsx(
3626
3387
  CardDrawer,
3627
3388
  {
3628
3389
  card: selectedCard,
@@ -3638,7 +3399,7 @@ function KanbanBoard({
3638
3399
  onRunTerminal: handleRunTerminal,
3639
3400
  onError
3640
3401
  }
3641
- ))
3402
+ )
3642
3403
  ] })
3643
3404
  ] });
3644
3405
  }
@@ -3663,7 +3424,6 @@ function composeAdapter(source, boardConfigUuid, version, cardSource) {
3663
3424
  transitionCard(input) {
3664
3425
  return cardSource.transitionCard(input);
3665
3426
  },
3666
- ...cardSource.deleteCard ? { deleteCard: (id) => cardSource.deleteCard(id) } : {},
3667
3427
  ...cardSource.createRunStream ? { createRunStream: (id) => cardSource.createRunStream(id) } : {}
3668
3428
  };
3669
3429
  }
@@ -4128,7 +3888,8 @@ var SSEClient = class {
4128
3888
  "Authorization": this.config.authHeader,
4129
3889
  "X-Org-Id": String(this.config.orgId),
4130
3890
  "Accept": "text/event-stream",
4131
- "Cache-Control": "no-cache"
3891
+ "Cache-Control": "no-cache",
3892
+ ...this.config.lastEventId ? { "Last-Event-ID": this.config.lastEventId } : {}
4132
3893
  },
4133
3894
  signal: this.abortController.signal
4134
3895
  });
@@ -4344,13 +4105,167 @@ function normalizeUrl(url) {
4344
4105
  return url;
4345
4106
  }
4346
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
+
4347
4246
  // src/streaming/stream-controller.ts
4348
4247
  var DEFAULT_STREAMING_CONFIG = {
4349
4248
  maxReconnectAttempts: 5,
4350
4249
  reconnectBaseDelayMs: 1e3,
4351
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.
4352
4259
  activityTimeoutMs: 3e4,
4353
- 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
4354
4269
  };
4355
4270
  var DEFAULT_POLLING_ENABLED = true;
4356
4271
  var ExecutionStreamControllerImpl = class {
@@ -4363,9 +4278,24 @@ var ExecutionStreamControllerImpl = class {
4363
4278
  seenEventIds = /* @__PURE__ */ new Set();
4364
4279
  _lastEventId = null;
4365
4280
  activityTimeoutId = null;
4366
- pollingIntervalId = null;
4367
4281
  finalExecution = null;
4368
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;
4369
4299
  // Event callbacks
4370
4300
  listeners = {
4371
4301
  log: [],
@@ -4394,6 +4324,39 @@ var ExecutionStreamControllerImpl = class {
4394
4324
  this.config = { ...DEFAULT_STREAMING_CONFIG, ...options.config };
4395
4325
  this.pollingEnabled = options.pollingEnabled ?? DEFAULT_POLLING_ENABLED;
4396
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
+ }
4397
4360
  /**
4398
4361
  * Get the last event ID (for resume support)
4399
4362
  */
@@ -4484,28 +4447,45 @@ var ExecutionStreamControllerImpl = class {
4484
4447
  console.log("[SDK:StreamController] executionId:", this.options.executionId);
4485
4448
  console.log("[SDK:StreamController] orgId:", this.options.orgId);
4486
4449
  console.log("[SDK:StreamController] apiKey:", authToken ? `${authToken.substring(0, 20)}...` : "MISSING");
4487
- this.sseClient = new SSEClient({
4450
+ if (this.sseClient) {
4451
+ this.sseClient.disconnect();
4452
+ this.sseClient = null;
4453
+ }
4454
+ const client = new SSEClient({
4488
4455
  url,
4489
4456
  authHeader: `Bearer ${authToken}`,
4490
4457
  orgId: this.options.orgId,
4491
4458
  fetch: this.options.fetch,
4492
- 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
+ },
4493
4466
  onError: (error) => {
4467
+ if (this.sseClient !== client) return;
4494
4468
  console.error("[SDK:StreamController] SSE error:", error);
4495
4469
  this.handleConnectionError(error);
4496
4470
  },
4497
4471
  onConnectionChange: (connected) => {
4472
+ if (this.sseClient !== client) return;
4498
4473
  console.log("[SDK:StreamController] SSE connection change:", connected);
4499
4474
  if (connected) {
4500
4475
  this.status = "connected";
4501
4476
  this.reconnectAttempts = 0;
4477
+ this.stopPolling();
4502
4478
  this.resetActivityTimeout();
4503
4479
  }
4504
- this.emit("connectionChange", { connected, usingFallback: false });
4480
+ this.emit("connectionChange", {
4481
+ connected,
4482
+ usingFallback: this.status === "fallback_polling"
4483
+ });
4505
4484
  }
4506
4485
  });
4486
+ this.sseClient = client;
4507
4487
  try {
4508
- await this.sseClient.connect();
4488
+ await client.connect();
4509
4489
  console.log("[SDK:StreamController] SSE connect() completed");
4510
4490
  } catch (error) {
4511
4491
  console.error("[SDK:StreamController] SSE connect() threw:", error);
@@ -4744,6 +4724,7 @@ var ExecutionStreamControllerImpl = class {
4744
4724
  handleComplete(data) {
4745
4725
  this.status = "closed";
4746
4726
  this.stopActivityTimeout();
4727
+ this.stopPolling();
4747
4728
  const sessionId = data.sessionId ?? this.options.sessionId;
4748
4729
  const execution = {
4749
4730
  uuid: this.options.executionId,
@@ -4770,6 +4751,7 @@ var ExecutionStreamControllerImpl = class {
4770
4751
  handleError(error) {
4771
4752
  this.status = "closed";
4772
4753
  this.stopActivityTimeout();
4754
+ this.stopPolling();
4773
4755
  this.finalError = error;
4774
4756
  this.emit("error", error);
4775
4757
  if (this.waitReject) {
@@ -4796,6 +4778,10 @@ var ExecutionStreamControllerImpl = class {
4796
4778
  this.handleError({ error: msg, errorCode: "stream_unrecoverable" });
4797
4779
  return;
4798
4780
  }
4781
+ if (this.status === "fallback_polling") {
4782
+ this.startPolling();
4783
+ return;
4784
+ }
4799
4785
  if (this.reconnectAttempts < this.config.maxReconnectAttempts) {
4800
4786
  this.status = "reconnecting";
4801
4787
  this.reconnectAttempts++;
@@ -4865,42 +4851,216 @@ var ExecutionStreamControllerImpl = class {
4865
4851
  }
4866
4852
  }
4867
4853
  /**
4868
- * 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.
4869
4858
  */
4870
4859
  startPolling() {
4871
- this.pollingIntervalId = setInterval(async () => {
4872
- try {
4873
- const execution = await this.options.getExecution(this.options.executionId);
4874
- for (const log of execution.logs) {
4875
- if (!this.seenEventIds.has(log.uuid)) {
4876
- this.seenEventIds.add(log.uuid);
4877
- this.emit("log", log);
4878
- }
4879
- }
4880
- if (execution.status === "completed") {
4881
- this.stopPolling();
4882
- this.handleComplete(execution);
4883
- } else if (execution.status === "failed" || execution.status === "cancelled") {
4884
- this.stopPolling();
4885
- this.handleError({
4886
- error: execution.error ?? `Execution ${execution.status}`,
4887
- errorCode: execution.status
4888
- });
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;
4889
4912
  }
4890
- } catch (error) {
4891
- console.error("Polling error:", error);
4892
4913
  }
4893
- }, 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;
4894
5006
  }
4895
5007
  /**
4896
- * Stop polling
5008
+ * Terminate the fallback loop and surface the reason.
5009
+ */
5010
+ failPolling(error, errorCode) {
5011
+ this.stopPolling();
5012
+ this.handleError({ error, errorCode });
5013
+ }
5014
+ /**
5015
+ * Stop polling, including the periodic SSE re-attempt that belongs to it.
4897
5016
  */
4898
5017
  stopPolling() {
4899
- if (this.pollingIntervalId) {
4900
- clearInterval(this.pollingIntervalId);
4901
- 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;
4902
5025
  }
4903
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;
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
+ });
5063
+ }
4904
5064
  /**
4905
5065
  * Reset activity timeout
4906
5066
  */
@@ -5031,12 +5191,23 @@ async function collectImagesFromStream(stream, options = {}) {
5031
5191
  });
5032
5192
  }
5033
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
+
5034
5204
  // src/resources/agents.ts
5035
5205
  var AgentsAPI = class extends BaseResourceAPI {
5036
5206
  constructor(http, getStreamConfig) {
5037
5207
  super(http, "/api/v1/agents");
5038
5208
  this.getStreamConfig = getStreamConfig;
5039
5209
  }
5210
+ getStreamConfig;
5040
5211
  // ===== LIST Operations =====
5041
5212
  /**
5042
5213
  * List all agents in the organization
@@ -5293,7 +5464,7 @@ var AgentsAPI = class extends BaseResourceAPI {
5293
5464
  * ```
5294
5465
  */
5295
5466
  async getExecution(executionId) {
5296
- return this.http.get(`/executions/${executionId}`);
5467
+ return this.http.get(executionPath(executionId));
5297
5468
  }
5298
5469
  // ===== EDIT Operations =====
5299
5470
  /**
@@ -5348,7 +5519,7 @@ var AgentsAPI = class extends BaseResourceAPI {
5348
5519
  */
5349
5520
  async cancelExecution(executionId) {
5350
5521
  return this.http.post(
5351
- `/executions/${executionId}/cancel`,
5522
+ executionCancelPath(executionId),
5352
5523
  void 0,
5353
5524
  { skipRetry: true }
5354
5525
  );
@@ -5849,6 +6020,7 @@ var ArtifactsAPI = class extends BaseResourceAPI {
5849
6020
  }
5850
6021
  return this.http.request({
5851
6022
  method: "POST",
6023
+ // KNOWN BROKEN — same missing `/api/v1` prefix as listArtifacts above.
5852
6024
  path: `/executions/${executionId}/artifacts`,
5853
6025
  body: formData,
5854
6026
  headers: {
@@ -5942,6 +6114,7 @@ var SessionsAPI = class extends BaseResourceAPI {
5942
6114
  super(http, "/api/v1/sessions");
5943
6115
  this.getStreamConfig = getStreamConfig;
5944
6116
  }
6117
+ getStreamConfig;
5945
6118
  // ===== LIST Operations =====
5946
6119
  /**
5947
6120
  * List all sessions in the organization
@@ -6121,7 +6294,7 @@ var SessionsAPI = class extends BaseResourceAPI {
6121
6294
  * Get execution result
6122
6295
  */
6123
6296
  async getExecution(executionId) {
6124
- return this.http.get(`/executions/${executionId}`);
6297
+ return this.http.get(executionPath(executionId));
6125
6298
  }
6126
6299
  // ===== EDIT Operations =====
6127
6300
  /**
@@ -6261,6 +6434,7 @@ var KanbanAPI = class extends BaseResourceAPI {
6261
6434
  super(http, "/api/v1/kanban");
6262
6435
  this.getStreamConfig = getStreamConfig;
6263
6436
  }
6437
+ getStreamConfig;
6264
6438
  // ===== BOARD =====
6265
6439
  /**
6266
6440
  * Get the current org's board (lanes in declared order). Auto-provisions
@@ -6395,7 +6569,7 @@ var KanbanAPI = class extends BaseResourceAPI {
6395
6569
  * @returns Execution details.
6396
6570
  */
6397
6571
  async getCardRun(executionId) {
6398
- return this.http.get(`/api/v1/agents/executions/${executionId}`);
6572
+ return this.http.get(executionPath(executionId));
6399
6573
  }
6400
6574
  /**
6401
6575
  * Create a stream controller for a card's run, keyed on the card's
@@ -6462,6 +6636,7 @@ var RemoteAgentsAPI = class extends BaseResourceAPI {
6462
6636
  super(http, "/api/v1/remote-agents");
6463
6637
  this.getStreamConfig = getStreamConfig;
6464
6638
  }
6639
+ getStreamConfig;
6465
6640
  /**
6466
6641
  * List all remote agents in the organization
6467
6642
  *
@@ -6595,7 +6770,7 @@ var RemoteAgentsAPI = class extends BaseResourceAPI {
6595
6770
  apiKey: config.apiKey,
6596
6771
  tokenProvider: config.tokenProvider,
6597
6772
  orgId: config.orgId,
6598
- getExecution: (id) => this.http.get(`/executions/${id}`)
6773
+ getExecution: (id) => this.http.get(executionPath(id))
6599
6774
  });
6600
6775
  return controller;
6601
6776
  }