@agent-native/core 0.144.1 → 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/analytics/index.d.ts +1 -1
- package/dist/client/analytics/index.js +1 -1
- package/dist/client/analytics-session.d.ts +13 -0
- package/dist/client/analytics-session.js +57 -0
- package/dist/client/analytics.d.ts +1 -0
- package/dist/client/analytics.js +1 -0
- 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/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- 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/track.js +7 -0
- package/dist/client/transcription/BuilderTranscriptionCta.js +27 -31
- package/dist/client/use-action.js +10 -1
- 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/file-upload/actions/upload-image.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/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/oauth-tokens/google-refresh.js +12 -19
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/action-routes.js +4 -2
- package/dist/server/agent-chat-plugin.js +26 -29
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-run-context.d.ts +4 -2
- package/dist/server/agent-run-context.js +5 -3
- package/dist/server/core-routes-plugin.js +2 -0
- 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/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/templates/workspace-core/.agents/skills/observability/SKILL.md +5 -1
- package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +20 -3
- package/dist/tracking/index.d.ts +1 -1
- package/dist/tracking/providers.js +22 -2
- package/dist/tracking/registry.d.ts +12 -2
- package/dist/tracking/registry.js +27 -3
- package/dist/tracking/types.d.ts +8 -0
- package/docs/content/observability.mdx +16 -0
- package/docs/content/tracking.mdx +17 -2
- 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
- package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +5 -1
- package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +20 -3
|
@@ -17,6 +17,7 @@ import { callAction } from "./use-action.js";
|
|
|
17
17
|
import { useChangeVersion } from "./use-change-version.js";
|
|
18
18
|
import { CHAT_MODEL_SELECTION_CHANGED_EVENT } from "./use-chat-models.js";
|
|
19
19
|
import { useChatThreads, } from "./use-chat-threads.js";
|
|
20
|
+
import { usePollLoop } from "./use-poll-loop.js";
|
|
20
21
|
import { cn } from "./utils.js";
|
|
21
22
|
/** The single path that hands a queued send to a mounted chat ref. */
|
|
22
23
|
function deliverPendingSend(ref, send) {
|
|
@@ -275,6 +276,7 @@ function chatTabStatusFromAgentTeamStatus(status) {
|
|
|
275
276
|
return isActiveAgentTeamStatus(status) ? "running" : "completed";
|
|
276
277
|
}
|
|
277
278
|
const STALE_THREAD_THRESHOLD_MS = 12 * 60 * 60 * 1000;
|
|
279
|
+
const DEFAULT_AGENT_TEAM_POLL_MS = 3000;
|
|
278
280
|
const DEFAULT_THREAD_URL_PARAM = "thread";
|
|
279
281
|
const THREAD_URL_CHANGED_EVENT = "agent-chat:url-thread-changed";
|
|
280
282
|
const hasOwn = Object.prototype.hasOwnProperty;
|
|
@@ -373,7 +375,7 @@ function runToAgentTeamTabInfo(run) {
|
|
|
373
375
|
status: run.status ?? "unknown",
|
|
374
376
|
};
|
|
375
377
|
}
|
|
376
|
-
export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderOverlay, contentHidden = false, apiUrl = agentNativePath("/_agent-native/agent-chat"), storageKey, restoreActiveThread = true, browserTabId, threadUrlSync = false, scope = null, ...props }) {
|
|
378
|
+
export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderOverlay, contentHidden = false, apiUrl = agentNativePath("/_agent-native/agent-chat"), storageKey, restoreActiveThread = true, browserTabId, threadUrlSync = false, scope = null, agentTeamPollMs = DEFAULT_AGENT_TEAM_POLL_MS, ...props }) {
|
|
377
379
|
const { enabled: threadUrlSyncEnabled, paramName: threadUrlParamName, routeThreadId, getPath: getThreadPath, navigate: navigateThreadUrl, } = resolveThreadUrlSync(threadUrlSync);
|
|
378
380
|
const threadRouteControlsActiveThread = threadUrlSyncEnabled &&
|
|
379
381
|
threadUrlSync !== true &&
|
|
@@ -896,107 +898,92 @@ export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderO
|
|
|
896
898
|
});
|
|
897
899
|
}
|
|
898
900
|
}, [isLoading, openTabIds, activeThreadId, createThread, writeThreadUrl]);
|
|
899
|
-
|
|
900
|
-
let stopped = false;
|
|
901
|
-
let timer = null;
|
|
901
|
+
usePollLoop(async (signal) => {
|
|
902
902
|
const runsUrl = `${apiUrl.replace(/\/$/, "")}/runs/list?goalId=agent-team`;
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
if (prev[info.threadId] !== info.status)
|
|
919
|
-
changed = true;
|
|
920
|
-
}
|
|
921
|
-
if (Object.keys(prev).length !== Object.keys(next).length) {
|
|
903
|
+
try {
|
|
904
|
+
const res = await fetch(runsUrl, { signal });
|
|
905
|
+
if (res.ok) {
|
|
906
|
+
const data = (await res.json());
|
|
907
|
+
const infos = Array.isArray(data.runs)
|
|
908
|
+
? data.runs
|
|
909
|
+
.map(runToAgentTeamTabInfo)
|
|
910
|
+
.filter((info) => Boolean(info))
|
|
911
|
+
: [];
|
|
912
|
+
setSubAgentStatuses((prev) => {
|
|
913
|
+
let changed = false;
|
|
914
|
+
const next = {};
|
|
915
|
+
for (const info of infos) {
|
|
916
|
+
next[info.threadId] = info.status;
|
|
917
|
+
if (prev[info.threadId] !== info.status)
|
|
922
918
|
changed = true;
|
|
919
|
+
}
|
|
920
|
+
if (Object.keys(prev).length !== Object.keys(next).length) {
|
|
921
|
+
changed = true;
|
|
922
|
+
}
|
|
923
|
+
return changed ? next : prev;
|
|
924
|
+
});
|
|
925
|
+
const openSet = new Set(openTabIdsRef.current);
|
|
926
|
+
const candidates = infos.filter((info) => !dismissedSubAgentTabsRef.current.has(info.threadId) &&
|
|
927
|
+
(openSet.has(info.parentThreadId) || openSet.has(info.threadId)));
|
|
928
|
+
if (candidates.length > 0) {
|
|
929
|
+
const shouldRefreshThreads = candidates.some((info) => !openSet.has(info.threadId) && openSet.has(info.parentThreadId));
|
|
930
|
+
const candidateParents = new Map(candidates.map((info) => [info.threadId, info.parentThreadId]));
|
|
931
|
+
setParentMap((prev) => {
|
|
932
|
+
let next = prev;
|
|
933
|
+
for (const info of candidates) {
|
|
934
|
+
if (next[info.threadId] === info.parentThreadId)
|
|
935
|
+
continue;
|
|
936
|
+
next =
|
|
937
|
+
next === prev
|
|
938
|
+
? { ...prev, [info.threadId]: info.parentThreadId }
|
|
939
|
+
: { ...next, [info.threadId]: info.parentThreadId };
|
|
923
940
|
}
|
|
924
|
-
return
|
|
941
|
+
return next;
|
|
925
942
|
});
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
(
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
});
|
|
956
|
-
setOpenTabIds((prev) => {
|
|
957
|
-
let next = prev;
|
|
958
|
-
for (const info of candidates) {
|
|
959
|
-
if (next.includes(info.threadId))
|
|
960
|
-
continue;
|
|
961
|
-
const parentIdx = next.indexOf(info.parentThreadId);
|
|
962
|
-
if (parentIdx === -1)
|
|
963
|
-
continue;
|
|
964
|
-
if (next === prev)
|
|
965
|
-
next = [...prev];
|
|
966
|
-
let insertIdx = parentIdx + 1;
|
|
967
|
-
while (insertIdx < next.length) {
|
|
968
|
-
const siblingParent = parentMapRef.current[next[insertIdx]] ||
|
|
969
|
-
candidateParents.get(next[insertIdx]);
|
|
970
|
-
if (siblingParent !== info.parentThreadId)
|
|
971
|
-
break;
|
|
972
|
-
insertIdx++;
|
|
973
|
-
}
|
|
974
|
-
next.splice(insertIdx, 0, info.threadId);
|
|
943
|
+
setSubAgentNames((prev) => {
|
|
944
|
+
let next = prev;
|
|
945
|
+
for (const info of candidates) {
|
|
946
|
+
if (!info.name || next[info.threadId] === info.name)
|
|
947
|
+
continue;
|
|
948
|
+
next =
|
|
949
|
+
next === prev
|
|
950
|
+
? { ...prev, [info.threadId]: info.name }
|
|
951
|
+
: { ...next, [info.threadId]: info.name };
|
|
952
|
+
}
|
|
953
|
+
return next;
|
|
954
|
+
});
|
|
955
|
+
setOpenTabIds((prev) => {
|
|
956
|
+
let next = prev;
|
|
957
|
+
for (const info of candidates) {
|
|
958
|
+
if (next.includes(info.threadId))
|
|
959
|
+
continue;
|
|
960
|
+
const parentIdx = next.indexOf(info.parentThreadId);
|
|
961
|
+
if (parentIdx === -1)
|
|
962
|
+
continue;
|
|
963
|
+
if (next === prev)
|
|
964
|
+
next = [...prev];
|
|
965
|
+
let insertIdx = parentIdx + 1;
|
|
966
|
+
while (insertIdx < next.length) {
|
|
967
|
+
const siblingParent = parentMapRef.current[next[insertIdx]] ||
|
|
968
|
+
candidateParents.get(next[insertIdx]);
|
|
969
|
+
if (siblingParent !== info.parentThreadId)
|
|
970
|
+
break;
|
|
971
|
+
insertIdx++;
|
|
975
972
|
}
|
|
976
|
-
|
|
977
|
-
});
|
|
978
|
-
if (shouldRefreshThreads) {
|
|
979
|
-
refreshThreads();
|
|
973
|
+
next.splice(insertIdx, 0, info.threadId);
|
|
980
974
|
}
|
|
975
|
+
return next;
|
|
976
|
+
});
|
|
977
|
+
if (shouldRefreshThreads) {
|
|
978
|
+
refreshThreads();
|
|
981
979
|
}
|
|
982
980
|
}
|
|
983
981
|
}
|
|
984
|
-
catch {
|
|
985
|
-
// Best effort: task cards and manual history still work if this poll fails.
|
|
986
|
-
}
|
|
987
|
-
finally {
|
|
988
|
-
if (!stopped) {
|
|
989
|
-
timer = setTimeout(hydrateAgentTeamTabs, 3000);
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
982
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
clearTimeout(timer);
|
|
998
|
-
};
|
|
999
|
-
}, [apiUrl, refreshThreads]);
|
|
983
|
+
catch {
|
|
984
|
+
// Best effort: task cards and manual history still work if this poll fails.
|
|
985
|
+
}
|
|
986
|
+
}, { intervalMs: agentTeamPollMs, pauseWhenHidden: true });
|
|
1000
987
|
// Focus the composer when switching tabs
|
|
1001
988
|
useEffect(() => {
|
|
1002
989
|
if (!activeThreadId)
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureClientException, captureError, captureException, captureMessage, configureTracking, getAnalyticsAnonymousId, getAnalyticsSessionId, getFirstTouchAttribution, getSessionReplayContext, getSessionReplayUrl, isErrorCaptureInstalled, maybeStartSessionReplay, setSentryUser, setTrackingContentCaptureEnabled, setTrackingIdentity, startSessionReplay, stopSessionReplay, trackAgentChatLifecycle, trackEvent, trackSessionStatus, type AgentChatLifecycleEvent, type CaptureExceptionContext, type CapturedExceptionEvent, type ClientCaptureContext, type ConfigureTrackingOptions, type ErrorCaptureConfigOptions, type ExceptionBreadcrumb, type ExceptionLevel, type FirstTouchAttribution, type SessionReplayContext, type SessionReplayLinkOptions, type SessionReplayOptions, type SessionReplayStartResult, type SessionReplayUrlMatcher, type TrackingIdentityUser, } from "../analytics.js";
|
|
1
|
+
export { AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureClientException, captureError, captureException, captureMessage, clearAnalyticsSessionId, configureTracking, getAnalyticsAnonymousId, getAnalyticsSessionId, getFirstTouchAttribution, getSessionReplayContext, getSessionReplayUrl, isErrorCaptureInstalled, maybeStartSessionReplay, setAnalyticsSessionId, setSentryUser, setTrackingContentCaptureEnabled, setTrackingIdentity, startSessionReplay, stopSessionReplay, trackAgentChatLifecycle, trackEvent, trackSessionStatus, type AgentChatLifecycleEvent, type CaptureExceptionContext, type CapturedExceptionEvent, type ClientCaptureContext, type ConfigureTrackingOptions, type ErrorCaptureConfigOptions, type ExceptionBreadcrumb, type ExceptionLevel, type FirstTouchAttribution, type SessionReplayContext, type SessionReplayLinkOptions, type SessionReplayOptions, type SessionReplayStartResult, type SessionReplayUrlMatcher, type TrackingIdentityUser, } from "../analytics.js";
|
|
2
2
|
export { track } from "../track.js";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureClientException, captureError, captureException, captureMessage, configureTracking, getAnalyticsAnonymousId, getAnalyticsSessionId, getFirstTouchAttribution, getSessionReplayContext, getSessionReplayUrl, isErrorCaptureInstalled, maybeStartSessionReplay, setSentryUser, setTrackingContentCaptureEnabled, setTrackingIdentity, startSessionReplay, stopSessionReplay, trackAgentChatLifecycle, trackEvent, trackSessionStatus, } from "../analytics.js";
|
|
1
|
+
export { AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureClientException, captureError, captureException, captureMessage, clearAnalyticsSessionId, configureTracking, getAnalyticsAnonymousId, getAnalyticsSessionId, getFirstTouchAttribution, getSessionReplayContext, getSessionReplayUrl, isErrorCaptureInstalled, maybeStartSessionReplay, setAnalyticsSessionId, setSentryUser, setTrackingContentCaptureEnabled, setTrackingIdentity, startSessionReplay, stopSessionReplay, trackAgentChatLifecycle, trackEvent, trackSessionStatus, } from "../analytics.js";
|
|
2
2
|
export { track } from "../track.js";
|
|
@@ -1,2 +1,15 @@
|
|
|
1
1
|
export declare function getOrCreateAnalyticsAnonymousId(): string | undefined;
|
|
2
|
+
/**
|
|
3
|
+
* Pin the browser session id an app wants every analytics event, agent run, and
|
|
4
|
+
* action request in this tab to carry. Returns the stored id, or `undefined`
|
|
5
|
+
* when there is no browser storage to pin it in (SSR) — the caller can tell
|
|
6
|
+
* those apart.
|
|
7
|
+
*
|
|
8
|
+
* Throws on an id the transport cannot carry rather than storing it.
|
|
9
|
+
*/
|
|
10
|
+
export declare function setAnalyticsSessionId(sessionId: string): string | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Drop a pinned id and let the next read start a fresh rotating session.
|
|
13
|
+
*/
|
|
14
|
+
export declare function clearAnalyticsSessionId(): void;
|
|
2
15
|
export declare function getOrCreateAnalyticsSessionId(): string | undefined;
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { serializeAnalyticsAnonymousIdCookie } from "../shared/analytics-anonymous-id.js";
|
|
2
2
|
const ANONYMOUS_ID_STORAGE_KEY = "agent-native.anonymous_id";
|
|
3
3
|
const SESSION_ID_STORAGE_KEY = "agent-native.session_id";
|
|
4
|
+
const SESSION_ID_PIN_STORAGE_KEY = "agent-native.session_id_pin";
|
|
4
5
|
const SESSION_LAST_ACTIVITY_STORAGE_KEY = "agent-native.session_last_activity";
|
|
5
6
|
// 30-minute idle timeout matches GA4 / Mixpanel defaults: a tab left open
|
|
6
7
|
// overnight starts a new session in the morning instead of stretching one visit.
|
|
7
8
|
const SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
9
|
+
// The server drops the session header at 128 characters
|
|
10
|
+
// (`readBrowserSessionIdHeader`), and `fetch` throws on a header value with
|
|
11
|
+
// non-ASCII or newline bytes. Both are rejected at the point the id is set so a
|
|
12
|
+
// bad id fails once, loudly, instead of silently unlinking every later request.
|
|
13
|
+
const MAX_SESSION_ID_LENGTH = 127;
|
|
14
|
+
const SAFE_SESSION_ID = /^[!-~]+$/;
|
|
8
15
|
function generateVisitorId() {
|
|
9
16
|
try {
|
|
10
17
|
if (typeof crypto !== "undefined" &&
|
|
@@ -35,6 +42,15 @@ function safeStorageSet(key, value) {
|
|
|
35
42
|
// private browsing / storage disabled -- best-effort
|
|
36
43
|
}
|
|
37
44
|
}
|
|
45
|
+
function safeStorageRemove(key) {
|
|
46
|
+
try {
|
|
47
|
+
window.localStorage.removeItem(key);
|
|
48
|
+
// coercion-ok: storage that refuses writes holds no key to clear, so the caller's end state already holds
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// private browsing / storage disabled -- best-effort
|
|
52
|
+
}
|
|
53
|
+
}
|
|
38
54
|
function syncAnalyticsAnonymousIdCookie(id) {
|
|
39
55
|
try {
|
|
40
56
|
const cookie = serializeAnalyticsAnonymousIdCookie(id);
|
|
@@ -57,10 +73,51 @@ export function getOrCreateAnalyticsAnonymousId() {
|
|
|
57
73
|
syncAnalyticsAnonymousIdCookie(id);
|
|
58
74
|
return id;
|
|
59
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Pin the browser session id an app wants every analytics event, agent run, and
|
|
78
|
+
* action request in this tab to carry. Returns the stored id, or `undefined`
|
|
79
|
+
* when there is no browser storage to pin it in (SSR) — the caller can tell
|
|
80
|
+
* those apart.
|
|
81
|
+
*
|
|
82
|
+
* Throws on an id the transport cannot carry rather than storing it.
|
|
83
|
+
*/
|
|
84
|
+
export function setAnalyticsSessionId(sessionId) {
|
|
85
|
+
const trimmed = typeof sessionId === "string" ? sessionId.trim() : "";
|
|
86
|
+
if (!trimmed ||
|
|
87
|
+
trimmed.length > MAX_SESSION_ID_LENGTH ||
|
|
88
|
+
!SAFE_SESSION_ID.test(trimmed)) {
|
|
89
|
+
throw new Error(`Invalid analytics session id: expected 1-${MAX_SESSION_ID_LENGTH} printable ASCII characters with no whitespace`);
|
|
90
|
+
}
|
|
91
|
+
if (typeof window === "undefined")
|
|
92
|
+
return undefined;
|
|
93
|
+
safeStorageSet(SESSION_ID_PIN_STORAGE_KEY, trimmed);
|
|
94
|
+
safeStorageSet(SESSION_ID_STORAGE_KEY, trimmed);
|
|
95
|
+
safeStorageSet(SESSION_LAST_ACTIVITY_STORAGE_KEY, String(Date.now()));
|
|
96
|
+
return trimmed;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Drop a pinned id and let the next read start a fresh rotating session.
|
|
100
|
+
*/
|
|
101
|
+
export function clearAnalyticsSessionId() {
|
|
102
|
+
if (typeof window === "undefined")
|
|
103
|
+
return;
|
|
104
|
+
safeStorageRemove(SESSION_ID_PIN_STORAGE_KEY);
|
|
105
|
+
safeStorageRemove(SESSION_ID_STORAGE_KEY);
|
|
106
|
+
safeStorageRemove(SESSION_LAST_ACTIVITY_STORAGE_KEY);
|
|
107
|
+
}
|
|
60
108
|
export function getOrCreateAnalyticsSessionId() {
|
|
61
109
|
if (typeof window === "undefined")
|
|
62
110
|
return undefined;
|
|
63
111
|
const now = Date.now();
|
|
112
|
+
// A pinned id opts out of idle rotation: the app chose it to correlate its
|
|
113
|
+
// own runs, so expiring it mid-workflow would split one correlated session
|
|
114
|
+
// into two that nothing can join back together.
|
|
115
|
+
const pinned = safeStorageGet(SESSION_ID_PIN_STORAGE_KEY);
|
|
116
|
+
if (pinned) {
|
|
117
|
+
safeStorageSet(SESSION_ID_STORAGE_KEY, pinned);
|
|
118
|
+
safeStorageSet(SESSION_LAST_ACTIVITY_STORAGE_KEY, String(now));
|
|
119
|
+
return pinned;
|
|
120
|
+
}
|
|
64
121
|
const lastActivityRaw = safeStorageGet(SESSION_LAST_ACTIVITY_STORAGE_KEY);
|
|
65
122
|
const lastActivity = lastActivityRaw
|
|
66
123
|
? Number.parseInt(lastActivityRaw, 10)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { clearAnalyticsSessionId, setAnalyticsSessionId, } from "./analytics-session.js";
|
|
1
2
|
import type { SessionReplayOptions, SessionReplayStartResult } from "./session-replay.js";
|
|
2
3
|
export { scrubUrl } from "./url-scrub.js";
|
|
3
4
|
export { addErrorBreadcrumb, captureException, captureMessage, isErrorCaptureInstalled, type CaptureExceptionContext, type CapturedExceptionEvent, type ExceptionBreadcrumb, type ExceptionLevel, } from "./error-capture.js";
|
package/dist/client/analytics.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { llmConnectionTrackingProperties, } from "../shared/llm-connection.js";
|
|
2
2
|
import { toPostHogExceptionProperties } from "../tracking/posthog-exception.js";
|
|
3
3
|
import { getOrCreateAnalyticsAnonymousId, getOrCreateAnalyticsSessionId, } from "./analytics-session.js";
|
|
4
|
+
export { clearAnalyticsSessionId, setAnalyticsSessionId, } from "./analytics-session.js";
|
|
4
5
|
import { fetchAgentEngineStatus, fetchAuthSessionStatus, } from "./client-status-requests.js";
|
|
5
6
|
import { installErrorCapture, } from "./error-capture.js";
|
|
6
7
|
import { isDynamicImportFailureMessage } from "./route-chunk-recovery.js";
|
|
@@ -1,11 +1,27 @@
|
|
|
1
|
+
import { createPollEngine } from "../shared/poll-engine.js";
|
|
1
2
|
import { agentNativePath } from "./api-path.js";
|
|
2
3
|
import { announceAgentNativeFrameReady, defaultAgentNativeHostCommands, requestAgentNativeHostActions, requestAgentNativeHostContext, runAgentNativeHostAction, sendAgentNativeHostCommand, } from "./host-bridge.js";
|
|
3
4
|
const DEFAULT_ENDPOINT = "/_agent-native/browser-sessions";
|
|
4
5
|
const DEFAULT_HEARTBEAT_MS = 5_000;
|
|
5
6
|
const DEFAULT_POLL_MS = 500;
|
|
7
|
+
const REQUEST_ABORT_MIN_MS = 10_000;
|
|
8
|
+
// Relaxed cadence floor applied while the tab is hidden — mirrors
|
|
9
|
+
// use-poll-loop.ts's `pauseWhenHidden: false` default so a backgrounded tab
|
|
10
|
+
// keeps heartbeating (the server-side session TTL still needs refreshing)
|
|
11
|
+
// instead of going fully silent.
|
|
12
|
+
const HIDDEN_INTERVAL_FLOOR_MS = 10_000;
|
|
13
|
+
function isDocumentHidden() {
|
|
14
|
+
return (typeof document !== "undefined" && document.visibilityState === "hidden");
|
|
15
|
+
}
|
|
6
16
|
function browserSessionId() {
|
|
7
17
|
return `browser-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
8
18
|
}
|
|
19
|
+
// Bounds a single request so a hung fetch can't leave the caller waiting
|
|
20
|
+
// forever on a wedged connection.
|
|
21
|
+
function requestAbortMs(options) {
|
|
22
|
+
const cadence = Math.min(options.pollMs ?? DEFAULT_POLL_MS, options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS);
|
|
23
|
+
return Math.max(REQUEST_ABORT_MIN_MS, cadence * 4);
|
|
24
|
+
}
|
|
9
25
|
function messageError(error) {
|
|
10
26
|
return error instanceof Error ? error : new Error(String(error));
|
|
11
27
|
}
|
|
@@ -35,26 +51,48 @@ async function readJsonResponse(response) {
|
|
|
35
51
|
return body;
|
|
36
52
|
}
|
|
37
53
|
async function postJson(options, path, body) {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
const controller = typeof AbortController === "undefined" ? null : new AbortController();
|
|
55
|
+
const timeoutId = controller
|
|
56
|
+
? setTimeout(() => controller.abort(), requestAbortMs(options))
|
|
57
|
+
: null;
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetchImpl(options)(endpointPath(options, path), {
|
|
60
|
+
method: "POST",
|
|
61
|
+
credentials: "include",
|
|
62
|
+
headers: {
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
"X-Agent-Native-CSRF": "1",
|
|
65
|
+
},
|
|
66
|
+
body: JSON.stringify(body ?? {}),
|
|
67
|
+
...(controller ? { signal: controller.signal } : {}),
|
|
68
|
+
});
|
|
69
|
+
return readJsonResponse(response);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
if (timeoutId)
|
|
73
|
+
clearTimeout(timeoutId);
|
|
74
|
+
}
|
|
48
75
|
}
|
|
49
76
|
async function deleteJson(options, path) {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
77
|
+
const controller = typeof AbortController === "undefined" ? null : new AbortController();
|
|
78
|
+
const timeoutId = controller
|
|
79
|
+
? setTimeout(() => controller.abort(), requestAbortMs(options))
|
|
80
|
+
: null;
|
|
81
|
+
try {
|
|
82
|
+
const response = await fetchImpl(options)(endpointPath(options, path), {
|
|
83
|
+
method: "DELETE",
|
|
84
|
+
credentials: "include",
|
|
85
|
+
headers: {
|
|
86
|
+
"X-Agent-Native-CSRF": "1",
|
|
87
|
+
},
|
|
88
|
+
...(controller ? { signal: controller.signal } : {}),
|
|
89
|
+
});
|
|
90
|
+
await readJsonResponse(response);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
if (timeoutId)
|
|
94
|
+
clearTimeout(timeoutId);
|
|
95
|
+
}
|
|
58
96
|
}
|
|
59
97
|
function hostRequestOptions(options) {
|
|
60
98
|
const { endpoint: _endpoint, sessionId: _sessionId, session: _session, getContext: _getContext, actions: _actions, commands: _commands, origin: _origin, label: _label, heartbeatMs: _heartbeatMs, pollMs: _pollMs, ttlMs: _ttlMs, fetch: _fetch, ...hostOptions } = options;
|
|
@@ -224,10 +262,7 @@ export function createAgentNativeBrowserSessionBridge(options = {}) {
|
|
|
224
262
|
let currentSessionId = options.sessionId ?? null;
|
|
225
263
|
let fallbackSessionId = null;
|
|
226
264
|
let started = false;
|
|
227
|
-
let
|
|
228
|
-
let pollTimer;
|
|
229
|
-
let refreshing = false;
|
|
230
|
-
let polling = false;
|
|
265
|
+
let onVisibility;
|
|
231
266
|
async function refreshRegistration() {
|
|
232
267
|
const direct = hasDirectHost(options);
|
|
233
268
|
const hostOptions = hostRequestOptions(options);
|
|
@@ -256,17 +291,6 @@ export function createAgentNativeBrowserSessionBridge(options = {}) {
|
|
|
256
291
|
});
|
|
257
292
|
return body.session;
|
|
258
293
|
}
|
|
259
|
-
async function heartbeat() {
|
|
260
|
-
if (refreshing)
|
|
261
|
-
return;
|
|
262
|
-
refreshing = true;
|
|
263
|
-
try {
|
|
264
|
-
await refreshRegistration();
|
|
265
|
-
}
|
|
266
|
-
finally {
|
|
267
|
-
refreshing = false;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
294
|
async function claimOnce() {
|
|
271
295
|
if (!currentSessionId) {
|
|
272
296
|
await refreshRegistration();
|
|
@@ -286,17 +310,27 @@ export function createAgentNativeBrowserSessionBridge(options = {}) {
|
|
|
286
310
|
}
|
|
287
311
|
return request;
|
|
288
312
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
313
|
+
// This is a plain factory function, not a React component, so it can't use
|
|
314
|
+
// the usePollLoop hook — createPollEngine gives each loop the same
|
|
315
|
+
// never-overlaps/never-stalls guarantees imperatively. The relaxed hidden
|
|
316
|
+
// cadence + visibilitychange wiring below mirrors what use-poll-loop.ts
|
|
317
|
+
// does internally for `pauseWhenHidden: false`.
|
|
318
|
+
const heartbeatEngine = createPollEngine(() => refreshRegistration().then(() => { }), {
|
|
319
|
+
intervalMs: () => {
|
|
320
|
+
const base = options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
|
|
321
|
+
return isDocumentHidden()
|
|
322
|
+
? Math.max(base, HIDDEN_INTERVAL_FLOOR_MS)
|
|
323
|
+
: base;
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
const pollEngine = createPollEngine(() => claimOnce().then(() => { }), {
|
|
327
|
+
intervalMs: () => {
|
|
328
|
+
const base = options.pollMs ?? DEFAULT_POLL_MS;
|
|
329
|
+
return isDocumentHidden()
|
|
330
|
+
? Math.max(base, HIDDEN_INTERVAL_FLOOR_MS)
|
|
331
|
+
: base;
|
|
332
|
+
},
|
|
333
|
+
});
|
|
300
334
|
const bridge = {
|
|
301
335
|
get sessionId() {
|
|
302
336
|
return currentSessionId;
|
|
@@ -308,22 +342,31 @@ export function createAgentNativeBrowserSessionBridge(options = {}) {
|
|
|
308
342
|
if (!hasDirectHost(options)) {
|
|
309
343
|
announceAgentNativeFrameReady(hostRequestOptions(options));
|
|
310
344
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
345
|
+
heartbeatEngine.start();
|
|
346
|
+
pollEngine.start();
|
|
347
|
+
onVisibility = () => {
|
|
348
|
+
if (isDocumentHidden()) {
|
|
349
|
+
heartbeatEngine.reschedule();
|
|
350
|
+
pollEngine.reschedule();
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
heartbeatEngine.pollNow();
|
|
354
|
+
pollEngine.pollNow();
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
315
358
|
return bridge;
|
|
316
359
|
},
|
|
317
360
|
stop() {
|
|
318
361
|
if (!started)
|
|
319
362
|
return;
|
|
320
363
|
started = false;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if (
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
364
|
+
heartbeatEngine.stop();
|
|
365
|
+
pollEngine.stop();
|
|
366
|
+
if (onVisibility) {
|
|
367
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
368
|
+
onVisibility = undefined;
|
|
369
|
+
}
|
|
327
370
|
if (currentSessionId) {
|
|
328
371
|
void deleteJson(options, `/${encodePathSegment(currentSessionId)}`).catch(() => { });
|
|
329
372
|
}
|
|
@@ -16,6 +16,7 @@ import { useEffect, useRef } from "react";
|
|
|
16
16
|
import { removeAgentChatContextItem, setAgentChatContextItem, } from "../agent-chat.js";
|
|
17
17
|
import { agentNativePath } from "../api-path.js";
|
|
18
18
|
import { deleteClientAppState, readClientAppState, setClientAppState, } from "../application-state.js";
|
|
19
|
+
import { usePollLoop } from "../use-poll-loop.js";
|
|
19
20
|
const NAVIGATION_PATH = agentNativePath("/_agent-native/application-state/navigation");
|
|
20
21
|
const NAVIGATE_PATH = agentNativePath("/_agent-native/application-state/navigate");
|
|
21
22
|
const POLL_INTERVAL_MS = 1500;
|
|
@@ -137,50 +138,40 @@ export function useDbAdminAgentSync({ table, mode, enabled = true, }) {
|
|
|
137
138
|
export function useNavigateConsumer(onNavigate, enabled = true) {
|
|
138
139
|
const handlerRef = useRef(onNavigate);
|
|
139
140
|
handlerRef.current = onNavigate;
|
|
141
|
+
const mountedRef = useRef(true);
|
|
140
142
|
useEffect(() => {
|
|
141
|
-
|
|
142
|
-
return;
|
|
143
|
-
let active = true;
|
|
144
|
-
let timer;
|
|
145
|
-
const tick = async () => {
|
|
146
|
-
if (!active)
|
|
147
|
-
return;
|
|
148
|
-
try {
|
|
149
|
-
const res = await fetch(NAVIGATE_PATH, {
|
|
150
|
-
method: "GET",
|
|
151
|
-
credentials: "include",
|
|
152
|
-
headers: headers(),
|
|
153
|
-
});
|
|
154
|
-
if (active && res.ok) {
|
|
155
|
-
const data = (await res.json());
|
|
156
|
-
if (data &&
|
|
157
|
-
data.view === "database" &&
|
|
158
|
-
typeof data.table === "string" &&
|
|
159
|
-
data.table) {
|
|
160
|
-
const target = data.table;
|
|
161
|
-
// Clear the one-shot command before acting so it fires once.
|
|
162
|
-
fetch(NAVIGATE_PATH, {
|
|
163
|
-
method: "DELETE",
|
|
164
|
-
credentials: "include",
|
|
165
|
-
headers: headers({ "X-Agent-Native-CSRF": "1" }),
|
|
166
|
-
}).catch(() => { });
|
|
167
|
-
handlerRef.current(target);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
catch {
|
|
172
|
-
// Ignore transient errors; the next tick retries.
|
|
173
|
-
}
|
|
174
|
-
finally {
|
|
175
|
-
if (active)
|
|
176
|
-
timer = setTimeout(tick, POLL_INTERVAL_MS);
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
timer = setTimeout(tick, POLL_INTERVAL_MS);
|
|
143
|
+
mountedRef.current = true;
|
|
180
144
|
return () => {
|
|
181
|
-
|
|
182
|
-
if (timer)
|
|
183
|
-
clearTimeout(timer);
|
|
145
|
+
mountedRef.current = false;
|
|
184
146
|
};
|
|
185
|
-
}, [
|
|
147
|
+
}, []);
|
|
148
|
+
usePollLoop(async (signal) => {
|
|
149
|
+
const res = await fetch(NAVIGATE_PATH, {
|
|
150
|
+
method: "GET",
|
|
151
|
+
credentials: "include",
|
|
152
|
+
headers: headers(),
|
|
153
|
+
signal,
|
|
154
|
+
});
|
|
155
|
+
if (!mountedRef.current || !res.ok)
|
|
156
|
+
return;
|
|
157
|
+
const data = (await res.json());
|
|
158
|
+
if (data &&
|
|
159
|
+
data.view === "database" &&
|
|
160
|
+
typeof data.table === "string" &&
|
|
161
|
+
data.table) {
|
|
162
|
+
const target = data.table;
|
|
163
|
+
// Clear the one-shot command before acting so it fires once.
|
|
164
|
+
fetch(NAVIGATE_PATH, {
|
|
165
|
+
method: "DELETE",
|
|
166
|
+
credentials: "include",
|
|
167
|
+
headers: headers({ "X-Agent-Native-CSRF": "1" }),
|
|
168
|
+
}).catch(() => { });
|
|
169
|
+
handlerRef.current(target);
|
|
170
|
+
}
|
|
171
|
+
}, {
|
|
172
|
+
intervalMs: POLL_INTERVAL_MS,
|
|
173
|
+
leading: false,
|
|
174
|
+
pauseWhenHidden: true,
|
|
175
|
+
enabled,
|
|
176
|
+
});
|
|
186
177
|
}
|
|
@@ -15,6 +15,8 @@ export interface AgentDynamicSuggestionsConfig {
|
|
|
15
15
|
includeStatic?: boolean;
|
|
16
16
|
/** Optional app-specific deterministic suggestion builder. */
|
|
17
17
|
getSuggestions?: (context: AgentDynamicSuggestionContext) => string[];
|
|
18
|
+
/** Safety-net refresh cadence in ms, for updates that don't bump app-state version. Default 30_000. */
|
|
19
|
+
pollMs?: number;
|
|
18
20
|
}
|
|
19
21
|
export type AgentDynamicSuggestionsOption = boolean | AgentDynamicSuggestionsConfig;
|
|
20
22
|
export interface AgentDynamicSuggestionsResult {
|
|
@@ -33,6 +35,7 @@ interface NormalizedAgentDynamicSuggestionsConfig {
|
|
|
33
35
|
max: number;
|
|
34
36
|
includeStatic: boolean;
|
|
35
37
|
getSuggestions?: (context: AgentDynamicSuggestionContext) => string[];
|
|
38
|
+
pollMs: number;
|
|
36
39
|
}
|
|
37
40
|
export declare function normalizeAgentDynamicSuggestionsConfig(option?: AgentDynamicSuggestionsOption): NormalizedAgentDynamicSuggestionsConfig;
|
|
38
41
|
export declare function buildDynamicAgentSuggestions(context: AgentDynamicSuggestionContext): string[];
|