@agent-native/core 0.85.3 → 0.85.5

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.
@@ -67,8 +67,11 @@ function createUserMessageRunConfig(references, requestMode, recoveryAction, tra
67
67
  const PENDING_SELECTION_KEY = "pending-selection-context";
68
68
  const ACTIVE_RUN_CLEAR_TIMEOUT_MS = 5_000;
69
69
  const ACTIVE_RUN_STUCK_THRESHOLD_MS = 90_000;
70
+ const BACKGROUND_ACTIVE_RUN_STUCK_THRESHOLD_MS = 13 * 60_000;
70
71
  const ACTIVE_RUN_POLL_INTERVAL_MS = 150;
71
72
  const AUTO_RESUME_STATUS_TIMEOUT_MS = 30_000;
73
+ const MAX_RECONNECT_AUTO_RECOVERIES = 3;
74
+ const RECONNECT_NO_PROGRESS_CONTINUE_MESSAGE = "Continue from where you stopped. Use the partial work above, verify what succeeded, and finish the original request. Do not rerun the exact same failed tool input unless the failure was transient or the user explicitly asked for an exact rerun. Prefer dedicated app actions over raw database edits when they exist.";
72
75
  // How long a single activity (model call, tool prep, long tool) must stay
73
76
  // in-flight before its label is surfaced in the running indicator. Below this
74
77
  // the indicator stays a steady "Thinking" so normal fast turns don't flicker
@@ -82,12 +85,19 @@ function isReplayableTerminalRun(runInfo) {
82
85
  dispatchMode.startsWith("background") &&
83
86
  runInfo.terminalReason === "run_timeout");
84
87
  }
88
+ function activeRunStuckThresholdMs(runInfo) {
89
+ const dispatchMode = typeof runInfo.dispatchMode === "string" ? runInfo.dispatchMode : "";
90
+ return dispatchMode.startsWith("background")
91
+ ? BACKGROUND_ACTIVE_RUN_STUCK_THRESHOLD_MS
92
+ : ACTIVE_RUN_STUCK_THRESHOLD_MS;
93
+ }
85
94
  function activeRunLooksStale(runInfo) {
86
95
  const lastProgressAt = typeof runInfo.lastProgressAt === "number" ? runInfo.lastProgressAt : null;
87
96
  const nowMs = typeof runInfo.serverNow === "number" ? runInfo.serverNow : Date.now();
97
+ const thresholdMs = activeRunStuckThresholdMs(runInfo);
88
98
  return (runInfo.status === "running" &&
89
99
  lastProgressAt != null &&
90
- nowMs - lastProgressAt > ACTIVE_RUN_STUCK_THRESHOLD_MS);
100
+ nowMs - lastProgressAt > thresholdMs);
91
101
  }
92
102
  /**
93
103
  * Decide whether a reconnect should give up with "no progress". The signal is
@@ -856,6 +866,8 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
856
866
  const reconnectTailOnlyRef = useRef(false);
857
867
  const reconnectCanMaterializeRef = useRef(false);
858
868
  const reconnectAbortRef = useRef(null);
869
+ const reconnectAutoRecoveryCountRef = useRef(0);
870
+ const [pendingReconnectRecovery, setPendingReconnectRecovery] = useState(null);
859
871
  // Nuclear stop: user clicked stop. Clears the stop button/indicator AND
860
872
  // lets new submissions go through immediately — prevents the "stuck
861
873
  // queueing forever" state where isReconnecting or isRuntimeRunning gets
@@ -1148,6 +1160,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1148
1160
  const abortCtrl = new AbortController();
1149
1161
  reconnectAbortRef.current = abortCtrl;
1150
1162
  let reconnectTerminalReason = null;
1163
+ const reconnectStuckThresholdMs = activeRunStuckThresholdMs(runInfo);
1151
1164
  const watchdog = setInterval(async () => {
1152
1165
  try {
1153
1166
  const res = await fetch(`${apiUrl}/runs/active?threadId=${encodeURIComponent(threadId)}`);
@@ -1188,6 +1201,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1188
1201
  if (!reconnectProgressTimedOut({
1189
1202
  lastProgressAt: lastReconnectProgressAt,
1190
1203
  now: Date.now(),
1204
+ thresholdMs: reconnectStuckThresholdMs,
1191
1205
  })) {
1192
1206
  return;
1193
1207
  }
@@ -1351,6 +1365,33 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1351
1365
  setReconnectFrozen(latestContent.length > 0);
1352
1366
  reconnectCanMaterializeRef.current = latestContent.length > 0;
1353
1367
  }
1368
+ const canAutoRecoverReconnect = reconnectTerminalReason !== "run_timeout" &&
1369
+ reconnectAutoRecoveryCountRef.current <
1370
+ MAX_RECONNECT_AUTO_RECOVERIES;
1371
+ if (canAutoRecoverReconnect) {
1372
+ reconnectAutoRecoveryCountRef.current += 1;
1373
+ setRunErrorInfo(null);
1374
+ setDismissedRunErrorKey(null);
1375
+ clearActiveRunIfMatches(threadId, runId);
1376
+ reconnectAbortRef.current = null;
1377
+ setIsReconnecting(false);
1378
+ reconnectRunIdRef.current = null;
1379
+ reconnectTailOnlyRef.current = false;
1380
+ if (afterSeq > 0) {
1381
+ reconnectCanMaterializeRef.current = false;
1382
+ }
1383
+ window.dispatchEvent(new CustomEvent("agent-chat:auto-continue", {
1384
+ detail: { tabId: tabId || threadId },
1385
+ }));
1386
+ setPendingReconnectRecovery({
1387
+ id: Date.now(),
1388
+ message: RECONNECT_NO_PROGRESS_CONTINUE_MESSAGE,
1389
+ });
1390
+ window.dispatchEvent(new CustomEvent("agentNative.chatRunning", {
1391
+ detail: { isRunning: false, tabId: tabId || threadId },
1392
+ }));
1393
+ return;
1394
+ }
1354
1395
  setRunErrorInfo({
1355
1396
  message: reconnectTerminalReason === "run_timeout"
1356
1397
  ? "The previous background agent run reached its time limit before finishing. The partial work was preserved; continue or retry to pick up from here."
@@ -1401,6 +1442,9 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1401
1442
  if (loaded || afterSeq > 0 || latestContent.length === 0) {
1402
1443
  reconnectCanMaterializeRef.current = false;
1403
1444
  }
1445
+ if (loaded) {
1446
+ reconnectAutoRecoveryCountRef.current = 0;
1447
+ }
1404
1448
  window.dispatchEvent(new CustomEvent("agentNative.chatRunning", {
1405
1449
  detail: { isRunning: false, tabId: tabId || threadId },
1406
1450
  }));
@@ -2227,10 +2271,13 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2227
2271
  ]);
2228
2272
  // Keep the ref current so addToQueue can call it without a stale closure.
2229
2273
  stopActiveRunRef.current = stopActiveRun;
2230
- const addToQueue = useCallback(async (text, images, references, attachments, requestMode, intent = "queued", recoveryAction, includeComposerContext = false, trackInRunsTray = false) => {
2274
+ const addToQueue = useCallback(async (text, images, references, attachments, requestMode, intent = "queued", recoveryAction, includeComposerContext = false, trackInRunsTray = false, preserveReconnectAutoRecoveryBudget = false) => {
2231
2275
  if (!(await ensureAgentEngineReadyForSubmit())) {
2232
2276
  return;
2233
2277
  }
2278
+ if (!preserveReconnectAutoRecoveryBudget) {
2279
+ reconnectAutoRecoveryCountRef.current = 0;
2280
+ }
2234
2281
  materializeFrozenReconnectContent();
2235
2282
  setShowContinue(false);
2236
2283
  setLoopLimitInfo(null);
@@ -2416,6 +2463,16 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2416
2463
  appendThreadMessage,
2417
2464
  updateComposerContextItems,
2418
2465
  ]);
2466
+ useEffect(() => {
2467
+ if (!pendingReconnectRecovery)
2468
+ return;
2469
+ const recovery = pendingReconnectRecovery;
2470
+ const timer = window.setTimeout(() => {
2471
+ setPendingReconnectRecovery((current) => current?.id === recovery.id ? null : current);
2472
+ addToQueue(recovery.message, undefined, undefined, undefined, undefined, "queued", "continue", false, false, true);
2473
+ }, 0);
2474
+ return () => window.clearTimeout(timer);
2475
+ }, [addToQueue, pendingReconnectRecovery]);
2419
2476
  // Expose imperative handle
2420
2477
  useImperativeHandle(ref, () => ({
2421
2478
  sendMessage(text, images, options) {
@@ -2696,7 +2753,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2696
2753
  addToQueue("Continue from where you left off.", undefined, undefined, undefined, undefined, "queued", "continue");
2697
2754
  } })), shouldShowRunError && visibleRunError && (_jsx(RunErrorRecoveryCard, { info: visibleRunError, onContinue: () => {
2698
2755
  setRunErrorInfo(null);
2699
- addToQueue("Continue from where you stopped. Use the partial work above, verify what succeeded, and finish the original request. Do not rerun the exact same failed tool input unless the failure was transient or the user explicitly asked for an exact rerun. Prefer dedicated app actions over raw database edits when they exist.", undefined, undefined, undefined, undefined, "queued", "continue");
2756
+ addToQueue(RECONNECT_NO_PROGRESS_CONTINUE_MESSAGE, undefined, undefined, undefined, undefined, "queued", "continue");
2700
2757
  }, onRetry: () => {
2701
2758
  setRunErrorInfo(null);
2702
2759
  addToQueue(lastUserText