@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.
@@ -265,12 +265,9 @@ function useKanbanBoard(options) {
265
265
  ]);
266
266
  setBoard(nextBoard);
267
267
  setCards(cardList.cards);
268
- setSelectedCard((prev) => {
269
- if (!prev) return prev;
270
- const next = cardList.cards.find((c) => c.id === prev.id);
271
- if (!next) return prev;
272
- return next.run ? next : { ...next, run: prev.run };
273
- });
268
+ setSelectedCard(
269
+ (prev) => prev ? cardList.cards.find((c) => c.id === prev.id) ?? prev : prev
270
+ );
274
271
  } catch (err) {
275
272
  fail(err, "Failed to load board");
276
273
  } finally {
@@ -285,12 +282,9 @@ function useKanbanBoard(options) {
285
282
  ]);
286
283
  setBoard(nextBoard);
287
284
  setCards(cardList.cards);
288
- setSelectedCard((prev) => {
289
- if (!prev) return prev;
290
- const next = cardList.cards.find((c) => c.id === prev.id);
291
- if (!next) return prev;
292
- return next.run ? next : { ...next, run: prev.run };
293
- });
285
+ setSelectedCard(
286
+ (prev) => prev ? cardList.cards.find((c) => c.id === prev.id) ?? prev : prev
287
+ );
294
288
  } catch {
295
289
  }
296
290
  }, [adapter]);
@@ -311,7 +305,7 @@ function useKanbanBoard(options) {
311
305
  try {
312
306
  const full = await adapter.getCard(cardId);
313
307
  setSelectedCard(
314
- (prev) => prev == null || prev.id === cardId ? full : prev
308
+ (prev) => prev && prev.id === cardId ? full : prev
315
309
  );
316
310
  } catch (err) {
317
311
  if (!known) fail(err, "Failed to open card");
@@ -420,27 +414,6 @@ function useKanbanBoard(options) {
420
414
  },
421
415
  [refresh]
422
416
  );
423
- const deleteCard = react.useCallback(
424
- async (cardId) => {
425
- if (!adapter.deleteCard) return false;
426
- setError(null);
427
- const prevCards = cards;
428
- const prevSelected = selectedCard;
429
- setCards((prev) => prev.filter((c) => c.id !== cardId));
430
- setSelectedCard((prev) => prev && prev.id === cardId ? null : prev);
431
- try {
432
- await adapter.deleteCard(cardId);
433
- void silentRefresh();
434
- return true;
435
- } catch (err) {
436
- setCards(prevCards);
437
- setSelectedCard(prevSelected);
438
- fail(err, "Failed to delete card");
439
- return false;
440
- }
441
- },
442
- [adapter, cards, selectedCard, silentRefresh, fail]
443
- );
444
417
  return {
445
418
  board,
446
419
  lanes: board?.lanes ?? [],
@@ -456,7 +429,6 @@ function useKanbanBoard(options) {
456
429
  createCard,
457
430
  updateCard,
458
431
  transitionCard,
459
- deleteCard,
460
432
  handleRunTerminal
461
433
  };
462
434
  }
@@ -2750,9 +2722,6 @@ function KanbanCard({
2750
2722
  onClick,
2751
2723
  selected,
2752
2724
  dueState,
2753
- draggable,
2754
- onDragStart,
2755
- onDragEnd,
2756
2725
  className
2757
2726
  }) {
2758
2727
  const runStatus = card.run?.status;
@@ -2781,20 +2750,10 @@ function KanbanCard({
2781
2750
  onClick: () => onClick?.(card),
2782
2751
  "aria-pressed": selected,
2783
2752
  "aria-busy": isWorking,
2784
- draggable,
2785
- onDragStart: draggable ? (e) => {
2786
- e.dataTransfer.effectAllowed = "move";
2787
- e.dataTransfer.setData("text/plain", card.id);
2788
- onDragStart?.(card);
2789
- } : void 0,
2790
- onDragEnd: draggable ? () => onDragEnd?.() : void 0,
2791
2753
  className: cn9(
2792
- // Solid card: fill + border + a faint token top-sheen (adapts to theme),
2793
- // matching the host's cockpit cards (work-shell v2 visual pass).
2794
- "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",
2754
+ "relative w-full text-left rounded-lg border bg-card p-3 shadow-sm transition",
2795
2755
  "hover:shadow-md hover:border-primary/40",
2796
2756
  "focus:outline-none focus:ring-2 focus:ring-ring/50",
2797
- draggable && "cursor-grab active:cursor-grabbing",
2798
2757
  // The working card carries the SINGLE focal motion on the board (F11).
2799
2758
  isWorking ? "border-info/60 ring-2 ring-info/40 animate-pulse" : selected ? "border-primary/60 ring-2 ring-primary/30" : "border-border",
2800
2759
  className
@@ -2871,11 +2830,6 @@ function CapMeter({ count, cap, className }) {
2871
2830
  }
2872
2831
  );
2873
2832
  }
2874
- var AGENT_GROUPS = [
2875
- { kind: "cadra", label: "Cadra agents" },
2876
- { kind: "remote", label: "Your own agents" },
2877
- { kind: "human", label: "Teammates" }
2878
- ];
2879
2833
  function AgentKindIcon({ kind }) {
2880
2834
  const common = {
2881
2835
  viewBox: "0 0 24 24",
@@ -2918,55 +2872,17 @@ function OnlineDot({ online }) {
2918
2872
  }
2919
2873
  );
2920
2874
  }
2921
- function RemoteBadge() {
2922
- return /* @__PURE__ */ jsxRuntime.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" });
2923
- }
2924
2875
  function LaneAgentPicker({
2925
2876
  selected,
2926
2877
  options,
2927
2878
  onSelect,
2928
2879
  canEdit = true,
2929
2880
  lockedReason,
2930
- onEditAgent,
2931
2881
  className
2932
2882
  }) {
2933
2883
  const [open, setOpen] = react.useState(false);
2934
- const [placement, setPlacement] = react.useState(null);
2935
2884
  const rootRef = react.useRef(null);
2936
- const triggerRef = react.useRef(null);
2937
2885
  const editable = canEdit && !!onSelect && (options?.length ?? 0) > 0;
2938
- const openMenu = () => {
2939
- const el = triggerRef.current;
2940
- if (el && typeof window !== "undefined") {
2941
- const rect = el.getBoundingClientRect();
2942
- const vw = window.innerWidth || 0;
2943
- const vh = window.innerHeight || 0;
2944
- const margin = 8;
2945
- const gap = 4;
2946
- const desired = 288;
2947
- const width = Math.max(
2948
- Math.min(rect.width || 224, Math.max(vw - margin * 2, 160)),
2949
- 160
2950
- );
2951
- const left = Math.min(
2952
- Math.max(rect.left, margin),
2953
- Math.max(vw - width - margin, margin)
2954
- );
2955
- const spaceBelow = vh - rect.bottom - gap - margin;
2956
- const spaceAbove = rect.top - gap - margin;
2957
- const flipUp = spaceBelow < desired && spaceAbove > spaceBelow;
2958
- const maxHeight = Math.max(
2959
- Math.min(desired, flipUp ? spaceAbove : spaceBelow),
2960
- 120
2961
- );
2962
- setPlacement(
2963
- flipUp ? { bottom: Math.max(vh - rect.top + gap, margin), left, width, maxHeight } : { top: rect.bottom + gap, left, width, maxHeight }
2964
- );
2965
- } else {
2966
- setPlacement(null);
2967
- }
2968
- setOpen(true);
2969
- };
2970
2886
  react.useEffect(() => {
2971
2887
  if (!open) return;
2972
2888
  const onDocDown = (e) => {
@@ -3000,28 +2916,14 @@ function LaneAgentPicker({
3000
2916
  }
3001
2917
  ),
3002
2918
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: displayLabel }),
3003
- kind === "remote" && /* @__PURE__ */ jsxRuntime.jsx(RemoteBadge, {}),
3004
2919
  /* @__PURE__ */ jsxRuntime.jsx(OnlineDot, { online: selected?.online })
3005
2920
  ] });
3006
- const grouped = AGENT_GROUPS.map((group) => ({
3007
- ...group,
3008
- items: (options ?? []).filter((opt) => (opt.kind ?? "cadra") === group.kind)
3009
- })).filter((group) => group.items.length > 0);
3010
- const menuStyle = placement ? {
3011
- position: "fixed",
3012
- top: placement.top,
3013
- bottom: placement.bottom,
3014
- left: placement.left,
3015
- width: placement.width,
3016
- maxHeight: placement.maxHeight
3017
- } : void 0;
3018
2921
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: rootRef, className: cn9("relative mt-2 w-full", className), children: [
3019
2922
  editable ? /* @__PURE__ */ jsxRuntime.jsxs(
3020
2923
  "button",
3021
2924
  {
3022
- ref: triggerRef,
3023
2925
  type: "button",
3024
- onClick: () => open ? setOpen(false) : openMenu(),
2926
+ onClick: () => setOpen((o) => !o),
3025
2927
  "aria-haspopup": "listbox",
3026
2928
  "aria-expanded": open,
3027
2929
  "aria-label": `Assigned agent: ${displayLabel}. Change`,
@@ -3099,106 +3001,47 @@ function LaneAgentPicker({
3099
3001
  ),
3100
3002
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: lockedReason })
3101
3003
  ] }),
3102
- editable && open && /* @__PURE__ */ jsxRuntime.jsxs(
3103
- "div",
3004
+ editable && open && /* @__PURE__ */ jsxRuntime.jsx(
3005
+ "ul",
3104
3006
  {
3105
- style: menuStyle,
3007
+ role: "listbox",
3008
+ "aria-label": "Assign an agent",
3106
3009
  className: cn9(
3107
- "flex flex-col overflow-hidden rounded-lg border border-border bg-popover shadow-lg",
3108
- menuStyle ? "z-50" : "absolute z-20 mt-1 max-h-64 w-full"
3010
+ "absolute z-20 mt-1 max-h-64 w-full overflow-y-auto rounded-lg border border-border",
3011
+ "bg-popover p-1 shadow-lg"
3109
3012
  ),
3110
- children: [
3111
- /* @__PURE__ */ jsxRuntime.jsx(
3112
- "ul",
3113
- {
3114
- role: "listbox",
3115
- "aria-label": "Assign an agent",
3116
- className: "min-h-0 flex-1 overflow-y-auto p-1",
3117
- children: grouped.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("li", { role: "presentation", children: [
3118
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-2 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground", children: group.label }),
3119
- /* @__PURE__ */ jsxRuntime.jsx("ul", { role: "group", "aria-label": group.label, children: group.items.map((opt) => {
3120
- const isSelected = opt.id === selected?.id;
3121
- const optKind = opt.kind ?? "cadra";
3122
- return /* @__PURE__ */ jsxRuntime.jsx("li", { role: "option", "aria-selected": isSelected, children: /* @__PURE__ */ jsxRuntime.jsxs(
3123
- "button",
3124
- {
3125
- type: "button",
3126
- onClick: () => {
3127
- onSelect(opt.id);
3128
- setOpen(false);
3129
- },
3130
- className: cn9(
3131
- "flex min-h-11 w-full items-center gap-1.5 rounded-md px-2 text-left text-xs font-medium",
3132
- "hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring/50",
3133
- isSelected ? "text-primary" : "text-foreground"
3134
- ),
3135
- children: [
3136
- /* @__PURE__ */ jsxRuntime.jsx(
3137
- "span",
3138
- {
3139
- className: cn9(
3140
- "grid h-5 w-5 shrink-0 place-items-center rounded-full",
3141
- optKind === "cadra" ? "bg-primary/15 text-primary" : "bg-secondary text-foreground"
3142
- ),
3143
- children: /* @__PURE__ */ jsxRuntime.jsx(AgentKindIcon, { kind: optKind })
3144
- }
3145
- ),
3146
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate", children: opt.label }),
3147
- optKind === "remote" && /* @__PURE__ */ jsxRuntime.jsx(RemoteBadge, {}),
3148
- /* @__PURE__ */ jsxRuntime.jsx(OnlineDot, { online: opt.online }),
3149
- isSelected && /* @__PURE__ */ jsxRuntime.jsx(
3150
- "svg",
3151
- {
3152
- viewBox: "0 0 24 24",
3153
- fill: "none",
3154
- stroke: "currentColor",
3155
- strokeWidth: 2,
3156
- strokeLinecap: "round",
3157
- strokeLinejoin: "round",
3158
- "aria-hidden": true,
3159
- className: "h-3.5 w-3.5 shrink-0",
3160
- children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M20 6L9 17l-5-5" })
3161
- }
3162
- )
3163
- ]
3164
- }
3165
- ) }, opt.id);
3166
- }) })
3167
- ] }, group.kind))
3168
- }
3169
- ),
3170
- onEditAgent && selected && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t border-border p-1", children: /* @__PURE__ */ jsxRuntime.jsxs(
3013
+ children: options.map((opt) => {
3014
+ const isSelected = opt.id === selected?.id;
3015
+ return /* @__PURE__ */ jsxRuntime.jsx("li", { role: "option", "aria-selected": isSelected, children: /* @__PURE__ */ jsxRuntime.jsxs(
3171
3016
  "button",
3172
3017
  {
3173
3018
  type: "button",
3174
3019
  onClick: () => {
3175
- onEditAgent(selected.id);
3020
+ onSelect(opt.id);
3176
3021
  setOpen(false);
3177
3022
  },
3178
3023
  className: cn9(
3179
- "flex min-h-11 w-full items-center gap-1.5 rounded-md px-2 text-left text-xs font-medium text-foreground",
3180
- "hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring/50"
3024
+ "flex min-h-11 w-full items-center gap-1.5 rounded-md px-2 text-left text-xs font-medium",
3025
+ "hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring/50",
3026
+ isSelected ? "text-primary" : "text-foreground"
3181
3027
  ),
3182
3028
  children: [
3183
3029
  /* @__PURE__ */ jsxRuntime.jsx(
3184
- "svg",
3030
+ "span",
3185
3031
  {
3186
- viewBox: "0 0 24 24",
3187
- fill: "none",
3188
- stroke: "currentColor",
3189
- strokeWidth: 2,
3190
- strokeLinecap: "round",
3191
- strokeLinejoin: "round",
3192
- "aria-hidden": true,
3193
- className: "h-3.5 w-3.5 shrink-0 text-muted-foreground",
3194
- children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" })
3032
+ className: cn9(
3033
+ "grid h-5 w-5 shrink-0 place-items-center rounded-full",
3034
+ (opt.kind ?? "cadra") === "cadra" ? "bg-primary/15 text-primary" : "bg-secondary text-foreground"
3035
+ ),
3036
+ children: /* @__PURE__ */ jsxRuntime.jsx(AgentKindIcon, { kind: opt.kind ?? "cadra" })
3195
3037
  }
3196
3038
  ),
3197
- "Edit this agent"
3039
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate", children: opt.label }),
3040
+ /* @__PURE__ */ jsxRuntime.jsx(OnlineDot, { online: opt.online })
3198
3041
  ]
3199
3042
  }
3200
- ) })
3201
- ]
3043
+ ) }, opt.id);
3044
+ })
3202
3045
  }
3203
3046
  )
3204
3047
  ] });
@@ -3223,12 +3066,6 @@ function KanbanColumn({
3223
3066
  count,
3224
3067
  agentPicker,
3225
3068
  canEditHandlers,
3226
- draggableCards,
3227
- dragActive,
3228
- dropAllowed,
3229
- onCardDragStart,
3230
- onCardDragEnd,
3231
- onLaneDrop,
3232
3069
  className
3233
3070
  }) {
3234
3071
  const headerCount = count ?? cards.length;
@@ -3239,21 +3076,9 @@ function KanbanColumn({
3239
3076
  return /* @__PURE__ */ jsxRuntime.jsxs(
3240
3077
  "div",
3241
3078
  {
3242
- onDragOver: dropAllowed ? (e) => {
3243
- e.preventDefault();
3244
- e.dataTransfer.dropEffect = "move";
3245
- } : void 0,
3246
- onDrop: dropAllowed && onLaneDrop ? (e) => {
3247
- e.preventDefault();
3248
- onLaneDrop(lane);
3249
- } : void 0,
3250
3079
  className: cn9(
3251
- "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",
3080
+ "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",
3252
3081
  isScheduled && "border-border",
3253
- // MV4 drag affordance: allowed targets glow; other lanes dim so the
3254
- // valid drop zones read at a glance.
3255
- dragActive && dropAllowed && "border-primary/60 ring-2 ring-primary/40 bg-primary/5",
3256
- dragActive && !dropAllowed && "opacity-55",
3257
3082
  className
3258
3083
  ),
3259
3084
  children: [
@@ -3322,10 +3147,7 @@ function KanbanColumn({
3322
3147
  card,
3323
3148
  onClick: onCardClick,
3324
3149
  selected: card.id === selectedCardId,
3325
- dueState: dueStateFor(card, lane, now),
3326
- draggable: draggableCards,
3327
- onDragStart: onCardDragStart,
3328
- onDragEnd: onCardDragEnd
3150
+ dueState: dueStateFor(card, lane, now)
3329
3151
  },
3330
3152
  card.id
3331
3153
  )),
@@ -3485,8 +3307,6 @@ function KanbanBoard({
3485
3307
  laneAgentPicker,
3486
3308
  scheduleStatus,
3487
3309
  focusCardId,
3488
- renderCardDrawer,
3489
- enableDrag,
3490
3310
  className
3491
3311
  }) {
3492
3312
  const {
@@ -3501,7 +3321,6 @@ function KanbanBoard({
3501
3321
  createCard,
3502
3322
  updateCard,
3503
3323
  transitionCard,
3504
- deleteCard,
3505
3324
  handleRunTerminal
3506
3325
  } = useKanbanBoard({ adapter, onError, onCardCreated, pollIntervalMs });
3507
3326
  const lastFocusedRef = react.useRef(void 0);
@@ -3546,44 +3365,6 @@ function KanbanBoard({
3546
3365
  (cardId, toLane) => transitionCard({ cardId, targetLane: toLane }),
3547
3366
  [transitionCard]
3548
3367
  );
3549
- const [dragging, setDragging] = react.useState(null);
3550
- const draggingRef = react.useRef(null);
3551
- const dropInFlightRef = react.useRef(null);
3552
- const allowedDropLanes = react.useMemo(() => {
3553
- if (!dragging || !board?.transitions) return /* @__PURE__ */ new Set();
3554
- return new Set(
3555
- board.transitions.filter((t) => t.fromLane === dragging.lane && t.manualAllowed).map((t) => t.toLane)
3556
- );
3557
- }, [dragging, board?.transitions]);
3558
- const handleCardDragStart = react.useCallback((card) => {
3559
- const d = { id: card.id, lane: card.lane };
3560
- draggingRef.current = d;
3561
- setDragging(d);
3562
- }, []);
3563
- const handleCardDragEnd = react.useCallback(() => {
3564
- draggingRef.current = null;
3565
- setDragging(null);
3566
- }, []);
3567
- const handleLaneDrop = react.useCallback(
3568
- (lane) => {
3569
- const cur = draggingRef.current;
3570
- draggingRef.current = null;
3571
- setDragging(null);
3572
- if (!cur || lane.key === cur.lane) return;
3573
- if (dropInFlightRef.current === cur.id) return;
3574
- const allowed = (board?.transitions ?? []).some(
3575
- (t) => t.fromLane === cur.lane && t.toLane === lane.key && t.manualAllowed
3576
- );
3577
- if (!allowed) return;
3578
- dropInFlightRef.current = cur.id;
3579
- void Promise.resolve(transitionCard({ cardId: cur.id, targetLane: lane.key })).finally(
3580
- () => {
3581
- if (dropInFlightRef.current === cur.id) dropInFlightRef.current = null;
3582
- }
3583
- );
3584
- },
3585
- [board?.transitions, transitionCard]
3586
- );
3587
3368
  const initialLaneKey = lanes.find((l) => !l.terminal)?.key ?? lanes[0]?.key;
3588
3369
  const selectedLane = selectedCard ? lanes.find((l) => l.key === selectedCard.lane) : void 0;
3589
3370
  const canAdvanceSelected = !!selectedLane && !selectedLane.terminal;
@@ -3604,32 +3385,12 @@ function KanbanBoard({
3604
3385
  showCreate: enableCreate && lane.key === initialLaneKey,
3605
3386
  count: laneCounts?.[lane.key],
3606
3387
  agentPicker: laneAgentPicker?.(lane),
3607
- canEditHandlers,
3608
- draggableCards: enableDrag && enableAdvance,
3609
- dragActive: !!dragging,
3610
- dropAllowed: !!dragging && dragging.lane !== lane.key && allowedDropLanes.has(lane.key),
3611
- onCardDragStart: handleCardDragStart,
3612
- onCardDragEnd: handleCardDragEnd,
3613
- onLaneDrop: handleLaneDrop
3388
+ canEditHandlers
3614
3389
  },
3615
3390
  lane.id ?? lane.key
3616
3391
  ))
3617
3392
  ] }),
3618
- selectedCard && (renderCardDrawer ? renderCardDrawer({
3619
- card: selectedCard,
3620
- adapter,
3621
- lanes,
3622
- transitions: enableAdvance ? board?.transitions : void 0,
3623
- canAdvance: canAdvanceSelected,
3624
- onClose: closeCard,
3625
- onRetry: onRetryCard ? handleRetry : void 0,
3626
- onUpdate: enableEdit ? handleUpdate : void 0,
3627
- onAdvance: enableAdvance ? handleAdvance : void 0,
3628
- onTransition: enableAdvance ? handleTransition : void 0,
3629
- onDelete: adapter.deleteCard ? deleteCard : void 0,
3630
- onRunTerminal: handleRunTerminal,
3631
- onError
3632
- }) : /* @__PURE__ */ jsxRuntime.jsx(
3393
+ selectedCard && /* @__PURE__ */ jsxRuntime.jsx(
3633
3394
  CardDrawer,
3634
3395
  {
3635
3396
  card: selectedCard,
@@ -3645,7 +3406,7 @@ function KanbanBoard({
3645
3406
  onRunTerminal: handleRunTerminal,
3646
3407
  onError
3647
3408
  }
3648
- ))
3409
+ )
3649
3410
  ] })
3650
3411
  ] });
3651
3412
  }
@@ -3670,7 +3431,6 @@ function composeAdapter(source, boardConfigUuid, version, cardSource) {
3670
3431
  transitionCard(input) {
3671
3432
  return cardSource.transitionCard(input);
3672
3433
  },
3673
- ...cardSource.deleteCard ? { deleteCard: (id) => cardSource.deleteCard(id) } : {},
3674
3434
  ...cardSource.createRunStream ? { createRunStream: (id) => cardSource.createRunStream(id) } : {}
3675
3435
  };
3676
3436
  }
@@ -4135,7 +3895,8 @@ var SSEClient = class {
4135
3895
  "Authorization": this.config.authHeader,
4136
3896
  "X-Org-Id": String(this.config.orgId),
4137
3897
  "Accept": "text/event-stream",
4138
- "Cache-Control": "no-cache"
3898
+ "Cache-Control": "no-cache",
3899
+ ...this.config.lastEventId ? { "Last-Event-ID": this.config.lastEventId } : {}
4139
3900
  },
4140
3901
  signal: this.abortController.signal
4141
3902
  });
@@ -4351,13 +4112,167 @@ function normalizeUrl(url) {
4351
4112
  return url;
4352
4113
  }
4353
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
+
4354
4253
  // src/streaming/stream-controller.ts
4355
4254
  var DEFAULT_STREAMING_CONFIG = {
4356
4255
  maxReconnectAttempts: 5,
4357
4256
  reconnectBaseDelayMs: 1e3,
4358
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.
4359
4266
  activityTimeoutMs: 3e4,
4360
- 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
4361
4276
  };
4362
4277
  var DEFAULT_POLLING_ENABLED = true;
4363
4278
  var ExecutionStreamControllerImpl = class {
@@ -4370,9 +4285,24 @@ var ExecutionStreamControllerImpl = class {
4370
4285
  seenEventIds = /* @__PURE__ */ new Set();
4371
4286
  _lastEventId = null;
4372
4287
  activityTimeoutId = null;
4373
- pollingIntervalId = null;
4374
4288
  finalExecution = null;
4375
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;
4376
4306
  // Event callbacks
4377
4307
  listeners = {
4378
4308
  log: [],
@@ -4401,6 +4331,39 @@ var ExecutionStreamControllerImpl = class {
4401
4331
  this.config = { ...DEFAULT_STREAMING_CONFIG, ...options.config };
4402
4332
  this.pollingEnabled = options.pollingEnabled ?? DEFAULT_POLLING_ENABLED;
4403
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
+ }
4404
4367
  /**
4405
4368
  * Get the last event ID (for resume support)
4406
4369
  */
@@ -4491,28 +4454,45 @@ var ExecutionStreamControllerImpl = class {
4491
4454
  console.log("[SDK:StreamController] executionId:", this.options.executionId);
4492
4455
  console.log("[SDK:StreamController] orgId:", this.options.orgId);
4493
4456
  console.log("[SDK:StreamController] apiKey:", authToken ? `${authToken.substring(0, 20)}...` : "MISSING");
4494
- this.sseClient = new SSEClient({
4457
+ if (this.sseClient) {
4458
+ this.sseClient.disconnect();
4459
+ this.sseClient = null;
4460
+ }
4461
+ const client = new SSEClient({
4495
4462
  url,
4496
4463
  authHeader: `Bearer ${authToken}`,
4497
4464
  orgId: this.options.orgId,
4498
4465
  fetch: this.options.fetch,
4499
- 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
+ },
4500
4473
  onError: (error) => {
4474
+ if (this.sseClient !== client) return;
4501
4475
  console.error("[SDK:StreamController] SSE error:", error);
4502
4476
  this.handleConnectionError(error);
4503
4477
  },
4504
4478
  onConnectionChange: (connected) => {
4479
+ if (this.sseClient !== client) return;
4505
4480
  console.log("[SDK:StreamController] SSE connection change:", connected);
4506
4481
  if (connected) {
4507
4482
  this.status = "connected";
4508
4483
  this.reconnectAttempts = 0;
4484
+ this.stopPolling();
4509
4485
  this.resetActivityTimeout();
4510
4486
  }
4511
- this.emit("connectionChange", { connected, usingFallback: false });
4487
+ this.emit("connectionChange", {
4488
+ connected,
4489
+ usingFallback: this.status === "fallback_polling"
4490
+ });
4512
4491
  }
4513
4492
  });
4493
+ this.sseClient = client;
4514
4494
  try {
4515
- await this.sseClient.connect();
4495
+ await client.connect();
4516
4496
  console.log("[SDK:StreamController] SSE connect() completed");
4517
4497
  } catch (error) {
4518
4498
  console.error("[SDK:StreamController] SSE connect() threw:", error);
@@ -4751,6 +4731,7 @@ var ExecutionStreamControllerImpl = class {
4751
4731
  handleComplete(data) {
4752
4732
  this.status = "closed";
4753
4733
  this.stopActivityTimeout();
4734
+ this.stopPolling();
4754
4735
  const sessionId = data.sessionId ?? this.options.sessionId;
4755
4736
  const execution = {
4756
4737
  uuid: this.options.executionId,
@@ -4777,6 +4758,7 @@ var ExecutionStreamControllerImpl = class {
4777
4758
  handleError(error) {
4778
4759
  this.status = "closed";
4779
4760
  this.stopActivityTimeout();
4761
+ this.stopPolling();
4780
4762
  this.finalError = error;
4781
4763
  this.emit("error", error);
4782
4764
  if (this.waitReject) {
@@ -4803,6 +4785,10 @@ var ExecutionStreamControllerImpl = class {
4803
4785
  this.handleError({ error: msg, errorCode: "stream_unrecoverable" });
4804
4786
  return;
4805
4787
  }
4788
+ if (this.status === "fallback_polling") {
4789
+ this.startPolling();
4790
+ return;
4791
+ }
4806
4792
  if (this.reconnectAttempts < this.config.maxReconnectAttempts) {
4807
4793
  this.status = "reconnecting";
4808
4794
  this.reconnectAttempts++;
@@ -4872,42 +4858,216 @@ var ExecutionStreamControllerImpl = class {
4872
4858
  }
4873
4859
  }
4874
4860
  /**
4875
- * 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.
4876
4865
  */
4877
4866
  startPolling() {
4878
- this.pollingIntervalId = setInterval(async () => {
4879
- try {
4880
- const execution = await this.options.getExecution(this.options.executionId);
4881
- for (const log of execution.logs) {
4882
- if (!this.seenEventIds.has(log.uuid)) {
4883
- this.seenEventIds.add(log.uuid);
4884
- this.emit("log", log);
4885
- }
4886
- }
4887
- if (execution.status === "completed") {
4888
- this.stopPolling();
4889
- this.handleComplete(execution);
4890
- } else if (execution.status === "failed" || execution.status === "cancelled") {
4891
- this.stopPolling();
4892
- this.handleError({
4893
- error: execution.error ?? `Execution ${execution.status}`,
4894
- errorCode: execution.status
4895
- });
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;
4896
4919
  }
4897
- } catch (error) {
4898
- console.error("Polling error:", error);
4899
4920
  }
4900
- }, 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;
4901
5013
  }
4902
5014
  /**
4903
- * Stop polling
5015
+ * Terminate the fallback loop and surface the reason.
5016
+ */
5017
+ failPolling(error, errorCode) {
5018
+ this.stopPolling();
5019
+ this.handleError({ error, errorCode });
5020
+ }
5021
+ /**
5022
+ * Stop polling, including the periodic SSE re-attempt that belongs to it.
4904
5023
  */
4905
5024
  stopPolling() {
4906
- if (this.pollingIntervalId) {
4907
- clearInterval(this.pollingIntervalId);
4908
- 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;
4909
5032
  }
4910
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;
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
+ });
5070
+ }
4911
5071
  /**
4912
5072
  * Reset activity timeout
4913
5073
  */
@@ -5038,12 +5198,23 @@ async function collectImagesFromStream(stream, options = {}) {
5038
5198
  });
5039
5199
  }
5040
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
+
5041
5211
  // src/resources/agents.ts
5042
5212
  var AgentsAPI = class extends BaseResourceAPI {
5043
5213
  constructor(http, getStreamConfig) {
5044
5214
  super(http, "/api/v1/agents");
5045
5215
  this.getStreamConfig = getStreamConfig;
5046
5216
  }
5217
+ getStreamConfig;
5047
5218
  // ===== LIST Operations =====
5048
5219
  /**
5049
5220
  * List all agents in the organization
@@ -5300,7 +5471,7 @@ var AgentsAPI = class extends BaseResourceAPI {
5300
5471
  * ```
5301
5472
  */
5302
5473
  async getExecution(executionId) {
5303
- return this.http.get(`/executions/${executionId}`);
5474
+ return this.http.get(executionPath(executionId));
5304
5475
  }
5305
5476
  // ===== EDIT Operations =====
5306
5477
  /**
@@ -5355,7 +5526,7 @@ var AgentsAPI = class extends BaseResourceAPI {
5355
5526
  */
5356
5527
  async cancelExecution(executionId) {
5357
5528
  return this.http.post(
5358
- `/executions/${executionId}/cancel`,
5529
+ executionCancelPath(executionId),
5359
5530
  void 0,
5360
5531
  { skipRetry: true }
5361
5532
  );
@@ -5856,6 +6027,7 @@ var ArtifactsAPI = class extends BaseResourceAPI {
5856
6027
  }
5857
6028
  return this.http.request({
5858
6029
  method: "POST",
6030
+ // KNOWN BROKEN — same missing `/api/v1` prefix as listArtifacts above.
5859
6031
  path: `/executions/${executionId}/artifacts`,
5860
6032
  body: formData,
5861
6033
  headers: {
@@ -5949,6 +6121,7 @@ var SessionsAPI = class extends BaseResourceAPI {
5949
6121
  super(http, "/api/v1/sessions");
5950
6122
  this.getStreamConfig = getStreamConfig;
5951
6123
  }
6124
+ getStreamConfig;
5952
6125
  // ===== LIST Operations =====
5953
6126
  /**
5954
6127
  * List all sessions in the organization
@@ -6128,7 +6301,7 @@ var SessionsAPI = class extends BaseResourceAPI {
6128
6301
  * Get execution result
6129
6302
  */
6130
6303
  async getExecution(executionId) {
6131
- return this.http.get(`/executions/${executionId}`);
6304
+ return this.http.get(executionPath(executionId));
6132
6305
  }
6133
6306
  // ===== EDIT Operations =====
6134
6307
  /**
@@ -6268,6 +6441,7 @@ var KanbanAPI = class extends BaseResourceAPI {
6268
6441
  super(http, "/api/v1/kanban");
6269
6442
  this.getStreamConfig = getStreamConfig;
6270
6443
  }
6444
+ getStreamConfig;
6271
6445
  // ===== BOARD =====
6272
6446
  /**
6273
6447
  * Get the current org's board (lanes in declared order). Auto-provisions
@@ -6402,7 +6576,7 @@ var KanbanAPI = class extends BaseResourceAPI {
6402
6576
  * @returns Execution details.
6403
6577
  */
6404
6578
  async getCardRun(executionId) {
6405
- return this.http.get(`/api/v1/agents/executions/${executionId}`);
6579
+ return this.http.get(executionPath(executionId));
6406
6580
  }
6407
6581
  /**
6408
6582
  * Create a stream controller for a card's run, keyed on the card's
@@ -6469,6 +6643,7 @@ var RemoteAgentsAPI = class extends BaseResourceAPI {
6469
6643
  super(http, "/api/v1/remote-agents");
6470
6644
  this.getStreamConfig = getStreamConfig;
6471
6645
  }
6646
+ getStreamConfig;
6472
6647
  /**
6473
6648
  * List all remote agents in the organization
6474
6649
  *
@@ -6602,7 +6777,7 @@ var RemoteAgentsAPI = class extends BaseResourceAPI {
6602
6777
  apiKey: config.apiKey,
6603
6778
  tokenProvider: config.tokenProvider,
6604
6779
  orgId: config.orgId,
6605
- getExecution: (id) => this.http.get(`/executions/${id}`)
6780
+ getExecution: (id) => this.http.get(executionPath(id))
6606
6781
  });
6607
6782
  return controller;
6608
6783
  }