@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.
Files changed (99) hide show
  1. package/corpus/templates/analytics/app/pages/DataSources.tsx +28 -4
  2. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +7 -3
  3. package/corpus/templates/analytics/server/plugins/dashboard-report-jobs.ts +13 -3
  4. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +64 -23
  5. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +69 -46
  6. package/corpus/templates/calendar/app/hooks/use-view-preferences.ts +35 -6
  7. package/corpus/templates/calendar/app/hooks/use-zoom-auth.ts +24 -3
  8. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +45 -6
  9. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +38 -5
  10. package/corpus/templates/clips/desktop/src/app.tsx +146 -45
  11. package/corpus/templates/clips/server/jobs/brain-export.ts +13 -6
  12. package/corpus/templates/clips/server/jobs/builder-media-compression.ts +10 -3
  13. package/corpus/templates/clips/server/jobs/meeting-reminders.ts +10 -3
  14. package/corpus/templates/clips/server/jobs/poll-calendars.ts +23 -21
  15. package/corpus/templates/clips/server/jobs/stale-meeting-sweeper.ts +10 -3
  16. package/corpus/templates/clips/server/jobs/transactional-emails.ts +10 -3
  17. package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +1 -0
  18. package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +15 -2
  19. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +82 -39
  20. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +31 -9
  21. package/corpus/templates/mail/server/plugins/mail-jobs.ts +36 -17
  22. package/dist/client/AssistantChat.js +73 -43
  23. package/dist/client/MultiTabAssistantChat.d.ts +3 -1
  24. package/dist/client/MultiTabAssistantChat.js +79 -92
  25. package/dist/client/analytics/index.d.ts +1 -1
  26. package/dist/client/analytics/index.js +1 -1
  27. package/dist/client/analytics-session.d.ts +13 -0
  28. package/dist/client/analytics-session.js +57 -0
  29. package/dist/client/analytics.d.ts +1 -0
  30. package/dist/client/analytics.js +1 -0
  31. package/dist/client/browser-session-bridge.js +97 -54
  32. package/dist/client/db-admin/useAgentSync.js +34 -43
  33. package/dist/client/dynamic-suggestions.d.ts +3 -0
  34. package/dist/client/dynamic-suggestions.js +54 -52
  35. package/dist/client/hooks/index.d.ts +1 -0
  36. package/dist/client/hooks/index.js +1 -0
  37. package/dist/client/i18n.d.ts +7 -0
  38. package/dist/client/i18n.js +12 -3
  39. package/dist/client/index.d.ts +1 -1
  40. package/dist/client/index.js +1 -1
  41. package/dist/client/integrations/useIntegrationStatus.d.ts +1 -1
  42. package/dist/client/integrations/useIntegrationStatus.js +9 -22
  43. package/dist/client/notifications/NotificationsBell.js +14 -9
  44. package/dist/client/progress/RunsTray.js +6 -5
  45. package/dist/client/settings/useBuilderStatus.js +95 -73
  46. package/dist/client/track.js +7 -0
  47. package/dist/client/transcription/BuilderTranscriptionCta.js +27 -31
  48. package/dist/client/use-action.js +10 -1
  49. package/dist/client/use-agent-engine-configured.js +21 -3
  50. package/dist/client/use-pausing-interval.d.ts +6 -0
  51. package/dist/client/use-pausing-interval.js +6 -0
  52. package/dist/client/use-poll-loop.d.ts +39 -0
  53. package/dist/client/use-poll-loop.js +73 -0
  54. package/dist/client/use-run-stuck-detection.js +41 -13
  55. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  56. package/dist/integrations/google-docs-poller.js +26 -7
  57. package/dist/integrations/pending-tasks-retry-job.js +17 -23
  58. package/dist/integrations/plugin.js +16 -15
  59. package/dist/integrations/remote-push-delivery-job.js +16 -25
  60. package/dist/integrations/remote-retry-job.js +19 -23
  61. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  62. package/dist/notifications/routes.d.ts +3 -3
  63. package/dist/oauth-tokens/google-refresh.js +12 -19
  64. package/dist/observability/routes.d.ts +3 -3
  65. package/dist/progress/routes.d.ts +1 -1
  66. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  67. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  68. package/dist/resources/handlers.d.ts +1 -1
  69. package/dist/secrets/routes.d.ts +3 -3
  70. package/dist/server/action-routes.js +4 -2
  71. package/dist/server/agent-chat-plugin.js +26 -29
  72. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  73. package/dist/server/agent-run-context.d.ts +4 -2
  74. package/dist/server/agent-run-context.js +5 -3
  75. package/dist/server/core-routes-plugin.js +2 -0
  76. package/dist/server/index.d.ts +1 -0
  77. package/dist/server/index.js +1 -0
  78. package/dist/server/interval-job.d.ts +25 -0
  79. package/dist/server/interval-job.js +19 -0
  80. package/dist/shared/index.d.ts +1 -0
  81. package/dist/shared/index.js +1 -0
  82. package/dist/shared/poll-engine.d.ts +49 -0
  83. package/dist/shared/poll-engine.js +148 -0
  84. package/dist/templates/default/.agents/skills/internationalization/SKILL.md +5 -0
  85. package/dist/templates/workspace-core/.agents/skills/internationalization/SKILL.md +5 -0
  86. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +5 -1
  87. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +20 -3
  88. package/dist/tracking/index.d.ts +1 -1
  89. package/dist/tracking/providers.js +22 -2
  90. package/dist/tracking/registry.d.ts +12 -2
  91. package/dist/tracking/registry.js +27 -3
  92. package/dist/tracking/types.d.ts +8 -0
  93. package/docs/content/observability.mdx +16 -0
  94. package/docs/content/tracking.mdx +17 -2
  95. package/package.json +4 -3
  96. package/src/templates/default/.agents/skills/internationalization/SKILL.md +5 -0
  97. package/src/templates/workspace-core/.agents/skills/internationalization/SKILL.md +5 -0
  98. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +5 -1
  99. 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
- useEffect(() => {
900
- let stopped = false;
901
- let timer = null;
901
+ usePollLoop(async (signal) => {
902
902
  const runsUrl = `${apiUrl.replace(/\/$/, "")}/runs/list?goalId=agent-team`;
903
- async function hydrateAgentTeamTabs() {
904
- try {
905
- const res = await fetch(runsUrl);
906
- if (res.ok) {
907
- const data = (await res.json());
908
- const infos = Array.isArray(data.runs)
909
- ? data.runs
910
- .map(runToAgentTeamTabInfo)
911
- .filter((info) => Boolean(info))
912
- : [];
913
- setSubAgentStatuses((prev) => {
914
- let changed = false;
915
- const next = {};
916
- for (const info of infos) {
917
- next[info.threadId] = info.status;
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 changed ? next : prev;
941
+ return next;
925
942
  });
926
- const openSet = new Set(openTabIdsRef.current);
927
- const candidates = infos.filter((info) => !dismissedSubAgentTabsRef.current.has(info.threadId) &&
928
- (openSet.has(info.parentThreadId) || openSet.has(info.threadId)));
929
- if (candidates.length > 0) {
930
- const shouldRefreshThreads = candidates.some((info) => !openSet.has(info.threadId) && openSet.has(info.parentThreadId));
931
- const candidateParents = new Map(candidates.map((info) => [info.threadId, info.parentThreadId]));
932
- setParentMap((prev) => {
933
- let next = prev;
934
- for (const info of candidates) {
935
- if (next[info.threadId] === info.parentThreadId)
936
- continue;
937
- next =
938
- next === prev
939
- ? { ...prev, [info.threadId]: info.parentThreadId }
940
- : { ...next, [info.threadId]: info.parentThreadId };
941
- }
942
- return next;
943
- });
944
- setSubAgentNames((prev) => {
945
- let next = prev;
946
- for (const info of candidates) {
947
- if (!info.name || next[info.threadId] === info.name)
948
- continue;
949
- next =
950
- next === prev
951
- ? { ...prev, [info.threadId]: info.name }
952
- : { ...next, [info.threadId]: info.name };
953
- }
954
- return next;
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
- return next;
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
- hydrateAgentTeamTabs();
994
- return () => {
995
- stopped = true;
996
- if (timer)
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";
@@ -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 response = await fetchImpl(options)(endpointPath(options, path), {
39
- method: "POST",
40
- credentials: "include",
41
- headers: {
42
- "Content-Type": "application/json",
43
- "X-Agent-Native-CSRF": "1",
44
- },
45
- body: JSON.stringify(body ?? {}),
46
- });
47
- return readJsonResponse(response);
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 response = await fetchImpl(options)(endpointPath(options, path), {
51
- method: "DELETE",
52
- credentials: "include",
53
- headers: {
54
- "X-Agent-Native-CSRF": "1",
55
- },
56
- });
57
- await readJsonResponse(response);
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 heartbeatTimer;
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
- async function poll() {
290
- if (polling)
291
- return;
292
- polling = true;
293
- try {
294
- await claimOnce();
295
- }
296
- finally {
297
- polling = false;
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
- void heartbeat();
312
- void poll();
313
- heartbeatTimer = setInterval(() => void heartbeat(), options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS);
314
- pollTimer = setInterval(() => void poll(), options.pollMs ?? DEFAULT_POLL_MS);
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
- if (heartbeatTimer)
322
- clearInterval(heartbeatTimer);
323
- if (pollTimer)
324
- clearInterval(pollTimer);
325
- heartbeatTimer = undefined;
326
- pollTimer = undefined;
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
- if (!enabled)
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
- active = false;
182
- if (timer)
183
- clearTimeout(timer);
145
+ mountedRef.current = false;
184
146
  };
185
- }, [enabled]);
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[];