@agent-native/core 0.144.2 → 0.144.3
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.
- package/corpus/templates/analytics/app/pages/DataSources.tsx +28 -4
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +7 -3
- package/corpus/templates/analytics/server/plugins/dashboard-report-jobs.ts +13 -3
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +64 -23
- package/corpus/templates/calendar/app/hooks/use-google-auth.ts +69 -46
- package/corpus/templates/calendar/app/hooks/use-view-preferences.ts +35 -6
- package/corpus/templates/calendar/app/hooks/use-zoom-auth.ts +24 -3
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +45 -6
- package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +38 -5
- package/corpus/templates/clips/desktop/src/app.tsx +146 -45
- package/corpus/templates/clips/server/jobs/brain-export.ts +13 -6
- package/corpus/templates/clips/server/jobs/builder-media-compression.ts +10 -3
- package/corpus/templates/clips/server/jobs/meeting-reminders.ts +10 -3
- package/corpus/templates/clips/server/jobs/poll-calendars.ts +23 -21
- package/corpus/templates/clips/server/jobs/stale-meeting-sweeper.ts +10 -3
- package/corpus/templates/clips/server/jobs/transactional-emails.ts +10 -3
- package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +1 -0
- package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +15 -2
- package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +82 -39
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +31 -9
- package/corpus/templates/mail/server/plugins/mail-jobs.ts +36 -17
- package/dist/client/AssistantChat.js +73 -43
- package/dist/client/MultiTabAssistantChat.d.ts +3 -1
- package/dist/client/MultiTabAssistantChat.js +79 -92
- package/dist/client/browser-session-bridge.js +97 -54
- package/dist/client/db-admin/useAgentSync.js +34 -43
- package/dist/client/dynamic-suggestions.d.ts +3 -0
- package/dist/client/dynamic-suggestions.js +54 -52
- package/dist/client/hooks/index.d.ts +1 -0
- package/dist/client/hooks/index.js +1 -0
- package/dist/client/i18n.d.ts +7 -0
- package/dist/client/i18n.js +12 -3
- package/dist/client/integrations/useIntegrationStatus.d.ts +1 -1
- package/dist/client/integrations/useIntegrationStatus.js +9 -22
- package/dist/client/notifications/NotificationsBell.js +14 -9
- package/dist/client/progress/RunsTray.js +6 -5
- package/dist/client/settings/useBuilderStatus.js +95 -73
- package/dist/client/transcription/BuilderTranscriptionCta.js +27 -31
- package/dist/client/use-agent-engine-configured.js +21 -3
- package/dist/client/use-pausing-interval.d.ts +6 -0
- package/dist/client/use-pausing-interval.js +6 -0
- package/dist/client/use-poll-loop.d.ts +39 -0
- package/dist/client/use-poll-loop.js +73 -0
- package/dist/client/use-run-stuck-detection.js +41 -13
- package/dist/collab/routes.d.ts +1 -1
- package/dist/integrations/google-docs-poller.js +26 -7
- package/dist/integrations/pending-tasks-retry-job.js +17 -23
- package/dist/integrations/plugin.js +16 -15
- package/dist/integrations/remote-push-delivery-job.js +16 -25
- package/dist/integrations/remote-retry-job.js +19 -23
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/oauth-tokens/google-refresh.js +12 -19
- package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/server/agent-chat-plugin.js +26 -29
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/interval-job.d.ts +25 -0
- package/dist/server/interval-job.js +19 -0
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/poll-engine.d.ts +49 -0
- package/dist/shared/poll-engine.js +148 -0
- package/dist/templates/default/.agents/skills/internationalization/SKILL.md +5 -0
- package/dist/templates/workspace-core/.agents/skills/internationalization/SKILL.md +5 -0
- package/dist/tracking/providers.js +10 -1
- package/package.json +4 -3
- package/src/templates/default/.agents/skills/internationalization/SKILL.md +5 -0
- package/src/templates/workspace-core/.agents/skills/internationalization/SKILL.md +5 -0
|
@@ -66,6 +66,16 @@ const STEPS = [
|
|
|
66
66
|
},
|
|
67
67
|
];
|
|
68
68
|
|
|
69
|
+
const DESKTOP_POLL_INTERVAL_MS = 1500;
|
|
70
|
+
const ADD_ACCOUNT_POLL_INTERVAL_MS = 2000;
|
|
71
|
+
// Bounds each poll's fetches so a hung request can't leave its in-flight
|
|
72
|
+
// guard stuck and stall the interval forever.
|
|
73
|
+
const DESKTOP_POLL_ABORT_MS = Math.max(10_000, DESKTOP_POLL_INTERVAL_MS * 4);
|
|
74
|
+
const ADD_ACCOUNT_POLL_ABORT_MS = Math.max(
|
|
75
|
+
10_000,
|
|
76
|
+
ADD_ACCOUNT_POLL_INTERVAL_MS * 4,
|
|
77
|
+
);
|
|
78
|
+
|
|
69
79
|
interface GoogleConnectBannerProps {
|
|
70
80
|
variant?: "banner" | "hero";
|
|
71
81
|
}
|
|
@@ -104,6 +114,8 @@ export function GoogleConnectBanner({
|
|
|
104
114
|
);
|
|
105
115
|
const desktopPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
106
116
|
const addAccountPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
117
|
+
const desktopPollInFlightRef = useRef(false);
|
|
118
|
+
const addAccountPollInFlightRef = useRef(false);
|
|
107
119
|
useEffect(() => {
|
|
108
120
|
return () => {
|
|
109
121
|
if (desktopPollRef.current) clearInterval(desktopPollRef.current);
|
|
@@ -130,40 +142,54 @@ export function GoogleConnectBanner({
|
|
|
130
142
|
const start = Date.now();
|
|
131
143
|
if (desktopPollRef.current) clearInterval(desktopPollRef.current);
|
|
132
144
|
desktopPollRef.current = setInterval(async () => {
|
|
145
|
+
if (document.hidden || desktopPollInFlightRef.current) return;
|
|
146
|
+
desktopPollInFlightRef.current = true;
|
|
147
|
+
const controller = new AbortController();
|
|
148
|
+
const abortTimer = setTimeout(
|
|
149
|
+
() => controller.abort(),
|
|
150
|
+
DESKTOP_POLL_ABORT_MS,
|
|
151
|
+
);
|
|
133
152
|
try {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
`/_agent-native/auth/desktop-exchange?flow_id=${flowId}`,
|
|
137
|
-
),
|
|
138
|
-
);
|
|
139
|
-
const data = await res.json();
|
|
140
|
-
if (data?.error) {
|
|
141
|
-
clearInterval(desktopPollRef.current!);
|
|
142
|
-
desktopPollRef.current = null;
|
|
143
|
-
setDesktopAuthIssue(data);
|
|
144
|
-
} else if (data?.token) {
|
|
145
|
-
clearInterval(desktopPollRef.current!);
|
|
146
|
-
desktopPollRef.current = null;
|
|
147
|
-
await fetch(
|
|
153
|
+
try {
|
|
154
|
+
const res = await fetch(
|
|
148
155
|
agentNativePath(
|
|
149
|
-
`/_agent-native/auth/
|
|
156
|
+
`/_agent-native/auth/desktop-exchange?flow_id=${flowId}`,
|
|
150
157
|
),
|
|
151
|
-
{
|
|
152
|
-
credentials: "include",
|
|
153
|
-
},
|
|
158
|
+
{ signal: controller.signal },
|
|
154
159
|
);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
const data = await res.json();
|
|
161
|
+
if (data?.error) {
|
|
162
|
+
clearInterval(desktopPollRef.current!);
|
|
163
|
+
desktopPollRef.current = null;
|
|
164
|
+
setDesktopAuthIssue(data);
|
|
165
|
+
} else if (data?.token) {
|
|
166
|
+
clearInterval(desktopPollRef.current!);
|
|
167
|
+
desktopPollRef.current = null;
|
|
168
|
+
await fetch(
|
|
169
|
+
agentNativePath(
|
|
170
|
+
`/_agent-native/auth/session?_session=${data.token}`,
|
|
171
|
+
),
|
|
172
|
+
{
|
|
173
|
+
credentials: "include",
|
|
174
|
+
signal: controller.signal,
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
window.location.reload();
|
|
178
|
+
} else if (Date.now() - start > 120_000) {
|
|
179
|
+
clearInterval(desktopPollRef.current!);
|
|
180
|
+
desktopPollRef.current = null;
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
if (Date.now() - start > 120_000) {
|
|
184
|
+
clearInterval(desktopPollRef.current!);
|
|
185
|
+
desktopPollRef.current = null;
|
|
186
|
+
}
|
|
164
187
|
}
|
|
188
|
+
} finally {
|
|
189
|
+
clearTimeout(abortTimer);
|
|
190
|
+
desktopPollInFlightRef.current = false;
|
|
165
191
|
}
|
|
166
|
-
},
|
|
192
|
+
}, DESKTOP_POLL_INTERVAL_MS);
|
|
167
193
|
}
|
|
168
194
|
|
|
169
195
|
const [authError, setAuthError] = useState<string | null>(null);
|
|
@@ -274,20 +300,37 @@ export function GoogleConnectBanner({
|
|
|
274
300
|
const prevCount = accounts.length;
|
|
275
301
|
if (addAccountPollRef.current) clearInterval(addAccountPollRef.current);
|
|
276
302
|
addAccountPollRef.current = setInterval(async () => {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
303
|
+
if (document.hidden || addAccountPollInFlightRef.current) return;
|
|
304
|
+
addAccountPollInFlightRef.current = true;
|
|
305
|
+
const controller = new AbortController();
|
|
306
|
+
const abortTimer = setTimeout(
|
|
307
|
+
() => controller.abort(),
|
|
308
|
+
ADD_ACCOUNT_POLL_ABORT_MS,
|
|
309
|
+
);
|
|
310
|
+
try {
|
|
311
|
+
const res = await fetch(
|
|
312
|
+
agentNativePath("/_agent-native/google/status"),
|
|
313
|
+
{ signal: controller.signal },
|
|
314
|
+
)
|
|
315
|
+
// coercion-ok: a failed probe and a not-yet-added-account response
|
|
316
|
+
// both mean "keep waiting"; this loop only acts on an observed
|
|
317
|
+
// account-count increase.
|
|
318
|
+
.catch(() => null);
|
|
319
|
+
if (res?.ok) {
|
|
320
|
+
const data = await res.json();
|
|
321
|
+
if (data.accounts?.length > prevCount) {
|
|
322
|
+
if (addAccountPollRef.current) {
|
|
323
|
+
clearInterval(addAccountPollRef.current);
|
|
324
|
+
addAccountPollRef.current = null;
|
|
325
|
+
}
|
|
326
|
+
window.location.reload();
|
|
286
327
|
}
|
|
287
|
-
window.location.reload();
|
|
288
328
|
}
|
|
329
|
+
} finally {
|
|
330
|
+
clearTimeout(abortTimer);
|
|
331
|
+
addAccountPollInFlightRef.current = false;
|
|
289
332
|
}
|
|
290
|
-
},
|
|
333
|
+
}, ADD_ACCOUNT_POLL_INTERVAL_MS);
|
|
291
334
|
// accounts.length is captured into prevCount above; including it in deps
|
|
292
335
|
// would tear down and recreate the interval whenever the count changes.
|
|
293
336
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -96,6 +96,10 @@ type SnoozeTarget = {
|
|
|
96
96
|
};
|
|
97
97
|
const COMPOSE_FULLSCREEN_PARAM = "composeFullscreen";
|
|
98
98
|
const SIDEBAR_COLLAPSE_KEY = "mail-sidebar-collapsed";
|
|
99
|
+
const ACCOUNT_POLL_INTERVAL_MS = 2000;
|
|
100
|
+
// Bounds the account-status poll so a hung fetch can't leave the in-flight
|
|
101
|
+
// guard stuck and stall the interval forever.
|
|
102
|
+
const ACCOUNT_POLL_ABORT_MS = Math.max(10_000, ACCOUNT_POLL_INTERVAL_MS * 4);
|
|
99
103
|
|
|
100
104
|
function AccountAvatar({
|
|
101
105
|
email,
|
|
@@ -2491,18 +2495,36 @@ function AccountPopover({
|
|
|
2491
2495
|
setWantAuthUrl(false);
|
|
2492
2496
|
window.open(authUrl.data.url, "_blank");
|
|
2493
2497
|
|
|
2498
|
+
let inFlight = false;
|
|
2494
2499
|
const interval = setInterval(async () => {
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2500
|
+
if (document.hidden || inFlight) return;
|
|
2501
|
+
inFlight = true;
|
|
2502
|
+
const controller = new AbortController();
|
|
2503
|
+
const abortTimer = setTimeout(
|
|
2504
|
+
() => controller.abort(),
|
|
2505
|
+
ACCOUNT_POLL_ABORT_MS,
|
|
2506
|
+
);
|
|
2507
|
+
try {
|
|
2508
|
+
const res = await fetch(
|
|
2509
|
+
agentNativePath("/_agent-native/google/status"),
|
|
2510
|
+
{ signal: controller.signal },
|
|
2511
|
+
)
|
|
2512
|
+
// coercion-ok: a failed probe and a not-yet-added-account response
|
|
2513
|
+
// both mean "keep waiting"; this loop only acts on an observed
|
|
2514
|
+
// account-count increase.
|
|
2515
|
+
.catch(() => null);
|
|
2516
|
+
if (res?.ok) {
|
|
2517
|
+
const data = await res.json();
|
|
2518
|
+
if (data.accounts?.length > accounts.length) {
|
|
2519
|
+
clearInterval(interval);
|
|
2520
|
+
window.location.reload();
|
|
2521
|
+
}
|
|
2503
2522
|
}
|
|
2523
|
+
} finally {
|
|
2524
|
+
clearTimeout(abortTimer);
|
|
2525
|
+
inFlight = false;
|
|
2504
2526
|
}
|
|
2505
|
-
},
|
|
2527
|
+
}, ACCOUNT_POLL_INTERVAL_MS);
|
|
2506
2528
|
|
|
2507
2529
|
return () => clearInterval(interval);
|
|
2508
2530
|
}, [wantAuthUrl, authUrl.data, accounts.length]);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { registerEvent } from "@agent-native/core/event-bus";
|
|
2
2
|
import { listOAuthAccounts } from "@agent-native/core/oauth-tokens";
|
|
3
|
+
import { startIntervalJob } from "@agent-native/core/server/interval-job";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
|
|
5
6
|
import { processAutomations } from "../lib/automation-engine.js";
|
|
@@ -18,6 +19,10 @@ import {
|
|
|
18
19
|
|
|
19
20
|
const INTERVAL_MS = 60_000; // 1 minute
|
|
20
21
|
const WATCH_RENEW_INTERVAL_MS = 12 * 60 * 60_000;
|
|
22
|
+
// Backstop for the whole tick (job sends + Gmail watch renewal, both outbound
|
|
23
|
+
// network calls with no timeout of their own), reported through the job's
|
|
24
|
+
// onError so a wedged tick is loud rather than silent.
|
|
25
|
+
const TICK_ABORT_MS = Math.max(10_000, INTERVAL_MS * 4);
|
|
21
26
|
let lastWatchRenewalAt = 0;
|
|
22
27
|
// Vite's dev server initializes Nitro plugins more than once during boot
|
|
23
28
|
// (initial load + post-init). Module-scope flag ensures the "skipping" log
|
|
@@ -124,24 +129,38 @@ export default () => {
|
|
|
124
129
|
return;
|
|
125
130
|
}
|
|
126
131
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
await processAutomations();
|
|
135
|
-
} catch (err) {
|
|
136
|
-
console.error("[mail-jobs] processAutomations failed:", err);
|
|
137
|
-
}
|
|
138
|
-
if (Date.now() - lastWatchRenewalAt > WATCH_RENEW_INTERVAL_MS) {
|
|
139
|
-
lastWatchRenewalAt = Date.now();
|
|
132
|
+
// The overlap guard and the hung-tick timeout both come from
|
|
133
|
+
// startIntervalJob rather than a module-level `running` flag here: these
|
|
134
|
+
// are outbound Google calls with no timeout of their own, and releasing the
|
|
135
|
+
// guard on the timeout while a hung call kept running is what let a tick
|
|
136
|
+
// overlap the next one and send duplicate mail.
|
|
137
|
+
startIntervalJob(
|
|
138
|
+
async () => {
|
|
140
139
|
try {
|
|
141
|
-
await
|
|
140
|
+
await processJobs();
|
|
142
141
|
} catch (err) {
|
|
143
|
-
console.error("[mail-jobs]
|
|
142
|
+
console.error("[mail-jobs] processJobs failed:", err);
|
|
144
143
|
}
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
try {
|
|
145
|
+
await processAutomations();
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.error("[mail-jobs] processAutomations failed:", err);
|
|
148
|
+
}
|
|
149
|
+
if (Date.now() - lastWatchRenewalAt > WATCH_RENEW_INTERVAL_MS) {
|
|
150
|
+
lastWatchRenewalAt = Date.now();
|
|
151
|
+
try {
|
|
152
|
+
await renewAllWatches();
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.error("[mail-jobs] renewAllWatches failed:", err);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
intervalMs: INTERVAL_MS,
|
|
160
|
+
timeoutMs: TICK_ABORT_MS,
|
|
161
|
+
leading: false,
|
|
162
|
+
onError: (err) =>
|
|
163
|
+
console.error("[mail-jobs] tick exceeded time budget:", err),
|
|
164
|
+
},
|
|
165
|
+
);
|
|
147
166
|
};
|
|
@@ -7,6 +7,7 @@ import { AssistantRuntimeProvider, useLocalRuntime, useThreadRuntime, useThread,
|
|
|
7
7
|
import { CompositeAttachmentAdapter } from "@assistant-ui/react";
|
|
8
8
|
import { IconArrowUp, IconClock, IconMessage, IconX, IconPlayerStopFilled, IconTerminal, IconAlertTriangle, IconChevronDown, IconRefresh, } from "@tabler/icons-react";
|
|
9
9
|
import { useState, useRef, useEffect, useCallback, useMemo, useLayoutEffect, forwardRef, useImperativeHandle, } from "react";
|
|
10
|
+
import { createPollEngine } from "../shared/poll-engine.js";
|
|
10
11
|
import { clearPendingTurnIfMatches, clearActiveRunIfMatches, getPendingTurn, getActiveRunActivityTool, getActiveRun, resolveReconnectAfterSeq, setActiveRun, updateActiveRunActivity, updateActiveRunSeq, } from "./active-run-state.js";
|
|
11
12
|
import { activeRunLooksAlive, createAgentChatAdapter, } from "./agent-chat-adapter.js";
|
|
12
13
|
import { appendAgentChatContextToMessage, formatAgentChatContextItemsForPrompt, getAgentChatContextState, isAgentChatSubmitCancelled, normalizeAgentChatContextItem, publishAgentChatContextItems, refreshAgentChatContext, reportAgentChatSubmitResult, subscribeAgentChatContext, } from "./agent-chat.js";
|
|
@@ -98,6 +99,13 @@ export function createUserMessageRunConfig(references, requestMode, recoveryActi
|
|
|
98
99
|
return options;
|
|
99
100
|
}
|
|
100
101
|
const PENDING_SELECTION_KEY = "pending-selection-context";
|
|
102
|
+
// Bounds an in-flight poll fetch so its own boolean in-flight guard is
|
|
103
|
+
// guaranteed to release even if the server never responds. Mirrors
|
|
104
|
+
// use-db-sync.ts's getPollAbortMs.
|
|
105
|
+
const POLL_ABORT_MIN_MS = 10_000;
|
|
106
|
+
function getPollAbortMs(interval) {
|
|
107
|
+
return Math.max(POLL_ABORT_MIN_MS, interval * 4);
|
|
108
|
+
}
|
|
101
109
|
const ACTIVE_RUN_CLEAR_TIMEOUT_MS = 5_000;
|
|
102
110
|
const ACTIVE_RUN_STUCK_THRESHOLD_MS = 90_000;
|
|
103
111
|
const BACKGROUND_ACTIVE_RUN_STUCK_THRESHOLD_MS = 13 * 60_000;
|
|
@@ -1836,7 +1844,11 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
1836
1844
|
}
|
|
1837
1845
|
return repo;
|
|
1838
1846
|
}, [threadRuntime]);
|
|
1839
|
-
|
|
1847
|
+
// Accepts an optional external `signal` so poll loops (e.g. the reconnect
|
|
1848
|
+
// thread-poll engine below) can cancel this fetch via their own timeout
|
|
1849
|
+
// instead of racing a second, separately-constructed AbortController.
|
|
1850
|
+
// Callers with no signal of their own keep the internal fallback timeout.
|
|
1851
|
+
const refreshThreadFromServer = useCallback(async (signal) => {
|
|
1840
1852
|
if (loadHistoryRepository) {
|
|
1841
1853
|
try {
|
|
1842
1854
|
const repo = await loadHistoryRepository();
|
|
@@ -1845,13 +1857,21 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
1845
1857
|
return importThreadData(repo);
|
|
1846
1858
|
}
|
|
1847
1859
|
catch {
|
|
1860
|
+
// coercion-ok: callers treat null as "keep the thread already
|
|
1861
|
+
// rendered", the same handling an absent repo needs.
|
|
1848
1862
|
return null;
|
|
1849
1863
|
}
|
|
1850
1864
|
}
|
|
1851
1865
|
if (!threadId)
|
|
1852
1866
|
return null;
|
|
1867
|
+
const ownAbort = !signal && typeof AbortController !== "undefined"
|
|
1868
|
+
? new AbortController()
|
|
1869
|
+
: null;
|
|
1870
|
+
const ownAbortTimer = ownAbort
|
|
1871
|
+
? setTimeout(() => ownAbort.abort(), getPollAbortMs(2000))
|
|
1872
|
+
: null;
|
|
1853
1873
|
try {
|
|
1854
|
-
const refreshRes = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}
|
|
1874
|
+
const refreshRes = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}`, { signal: signal ?? ownAbort?.signal });
|
|
1855
1875
|
if (!refreshRes.ok)
|
|
1856
1876
|
return null;
|
|
1857
1877
|
const refreshData = await refreshRes.json();
|
|
@@ -1860,8 +1880,14 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
1860
1880
|
return importThreadData(refreshData.threadData);
|
|
1861
1881
|
}
|
|
1862
1882
|
catch {
|
|
1883
|
+
// coercion-ok: an aborted or failed refresh keeps the thread already
|
|
1884
|
+
// rendered; the next poll tick retries.
|
|
1863
1885
|
return null;
|
|
1864
1886
|
}
|
|
1887
|
+
finally {
|
|
1888
|
+
if (ownAbortTimer)
|
|
1889
|
+
clearTimeout(ownAbortTimer);
|
|
1890
|
+
}
|
|
1865
1891
|
}, [apiUrl, importThreadData, loadHistoryRepository, threadId]);
|
|
1866
1892
|
const exportCleanThreadRepo = useCallback(() => ensureMessageMetadata(normalizeThreadRepository(threadRuntime.export())), [threadRuntime]);
|
|
1867
1893
|
const exportPersistableThreadRepo = useCallback(() => withLastAssistantRunDuration(exportCleanThreadRepo(), showRunningInUI ? null : lastChatRunDurationMs), [exportCleanThreadRepo, lastChatRunDurationMs, showRunningInUI]);
|
|
@@ -2026,37 +2052,38 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
2026
2052
|
reconnectAbortRef.current = abortCtrl;
|
|
2027
2053
|
let reconnectTerminalReason = null;
|
|
2028
2054
|
const reconnectStuckThresholdMs = activeRunStuckThresholdMs(runInfo);
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
// thread's run), this reader is now the duplicate — kill it. The
|
|
2041
|
-
// false→true runtime-transition effect also unwinds the UI state;
|
|
2042
|
-
// this covers the reader itself.
|
|
2043
|
-
if (isRuntimeRunningRef.current) {
|
|
2044
|
-
abortCtrl.abort();
|
|
2045
|
-
clearInterval(watchdog);
|
|
2046
|
-
return;
|
|
2047
|
-
}
|
|
2048
|
-
if (isReplayableTerminalRun(info)) {
|
|
2049
|
-
return;
|
|
2050
|
-
}
|
|
2051
|
-
if (info.status !== "running" || activeRunLooksStale(info)) {
|
|
2052
|
-
abortCtrl.abort();
|
|
2053
|
-
clearInterval(watchdog);
|
|
2054
|
-
}
|
|
2055
|
+
// This lives inside a useCallback, not component render, so it can't
|
|
2056
|
+
// use the usePollLoop hook (Rules of Hooks) — createPollEngine gives
|
|
2057
|
+
// the same never-overlaps/never-stalls guarantees imperatively.
|
|
2058
|
+
const watchdog = createPollEngine(async (signal) => {
|
|
2059
|
+
if (document.hidden)
|
|
2060
|
+
return;
|
|
2061
|
+
const res = await fetch(`${apiUrl}/runs/active?threadId=${encodeURIComponent(threadId)}`, { signal });
|
|
2062
|
+
if (!res.ok) {
|
|
2063
|
+
abortCtrl.abort();
|
|
2064
|
+
watchdog.stop();
|
|
2065
|
+
return;
|
|
2055
2066
|
}
|
|
2056
|
-
|
|
2057
|
-
|
|
2067
|
+
const info = (await res.json());
|
|
2068
|
+
// SINGLE-READER OWNERSHIP: if the adapter runtime came alive while
|
|
2069
|
+
// this reader was attached (user sent a message, adapter adopted the
|
|
2070
|
+
// thread's run), this reader is now the duplicate — kill it. The
|
|
2071
|
+
// false→true runtime-transition effect also unwinds the UI state;
|
|
2072
|
+
// this covers the reader itself.
|
|
2073
|
+
if (isRuntimeRunningRef.current) {
|
|
2074
|
+
abortCtrl.abort();
|
|
2075
|
+
watchdog.stop();
|
|
2076
|
+
return;
|
|
2058
2077
|
}
|
|
2059
|
-
|
|
2078
|
+
if (isReplayableTerminalRun(info)) {
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
if (info.status !== "running" || activeRunLooksStale(info)) {
|
|
2082
|
+
abortCtrl.abort();
|
|
2083
|
+
watchdog.stop();
|
|
2084
|
+
}
|
|
2085
|
+
}, { intervalMs: 1000 });
|
|
2086
|
+
watchdog.start();
|
|
2060
2087
|
let reconnectTimedOut = false;
|
|
2061
2088
|
// Idle deadline, NOT a total-duration cap. A long-but-healthy run (image
|
|
2062
2089
|
// generation emits `activity` heartbeats every few seconds) keeps
|
|
@@ -2082,7 +2109,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
2082
2109
|
}
|
|
2083
2110
|
reconnectTimedOut = true;
|
|
2084
2111
|
abortCtrl.abort();
|
|
2085
|
-
|
|
2112
|
+
watchdog.stop();
|
|
2086
2113
|
clearInterval(idleCheck);
|
|
2087
2114
|
}, 1000);
|
|
2088
2115
|
const streamReconnect = async () => {
|
|
@@ -2119,18 +2146,23 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
2119
2146
|
return "unknown";
|
|
2120
2147
|
}
|
|
2121
2148
|
};
|
|
2122
|
-
const
|
|
2123
|
-
?
|
|
2149
|
+
const threadPollEngine = afterSeq > 0
|
|
2150
|
+
? createPollEngine(async (signal) => {
|
|
2151
|
+
if (document.hidden)
|
|
2152
|
+
return;
|
|
2124
2153
|
if (reconnectRunIdRef.current !== runId)
|
|
2125
2154
|
return;
|
|
2126
|
-
// Adapter took over mid-poll — skip imports that would race
|
|
2127
|
-
// live stream and flicker tool cards back to older
|
|
2128
|
-
|
|
2155
|
+
// Adapter took over mid-poll — skip imports that would race
|
|
2156
|
+
// the live stream and flicker tool cards back to older
|
|
2157
|
+
// snapshots.
|
|
2158
|
+
if (isRuntimeRunningRef.current ||
|
|
2159
|
+
isAutoResumingRef.current) {
|
|
2129
2160
|
return;
|
|
2130
2161
|
}
|
|
2131
|
-
|
|
2132
|
-
}, 2000)
|
|
2162
|
+
await refreshThreadFromServer(signal);
|
|
2163
|
+
}, { intervalMs: 2000, leading: false })
|
|
2133
2164
|
: undefined;
|
|
2165
|
+
threadPollEngine?.start();
|
|
2134
2166
|
try {
|
|
2135
2167
|
const content = [];
|
|
2136
2168
|
latestContent = content;
|
|
@@ -2213,10 +2245,8 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
2213
2245
|
}
|
|
2214
2246
|
}
|
|
2215
2247
|
finally {
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
}
|
|
2219
|
-
clearInterval(watchdog);
|
|
2248
|
+
threadPollEngine?.stop();
|
|
2249
|
+
watchdog.stop();
|
|
2220
2250
|
clearInterval(idleCheck);
|
|
2221
2251
|
}
|
|
2222
2252
|
// A newer reader, live adapter, stop action, or component unmount took
|
|
@@ -66,5 +66,7 @@ export type MultiTabAssistantChatProps = Omit<AssistantChatProps, "tabId" | "thr
|
|
|
66
66
|
scope?: ChatThreadScope | null;
|
|
67
67
|
/** @deprecated Scope context is now rendered in the composer. */
|
|
68
68
|
showScopeBadge?: boolean;
|
|
69
|
+
/** Cadence for hydrating agent-team sub-agent tab status. Default: 3000. */
|
|
70
|
+
agentTeamPollMs?: number;
|
|
69
71
|
};
|
|
70
|
-
export declare function MultiTabAssistantChat({ showTabBar, renderHeader, renderOverlay, contentHidden, apiUrl, storageKey, restoreActiveThread, browserTabId, threadUrlSync, scope, ...props }: MultiTabAssistantChatProps): React.JSX.Element;
|
|
72
|
+
export declare function MultiTabAssistantChat({ showTabBar, renderHeader, renderOverlay, contentHidden, apiUrl, storageKey, restoreActiveThread, browserTabId, threadUrlSync, scope, agentTeamPollMs, ...props }: MultiTabAssistantChatProps): React.JSX.Element;
|