@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
@@ -77,6 +77,7 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
77
77
  }, z.core.$strip>>, {
78
78
  id?: undefined;
79
79
  deleted?: undefined;
80
+ found?: undefined;
80
81
  providers: {
81
82
  id: string;
82
83
  label: string;
@@ -88,15 +89,14 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
88
89
  }[];
89
90
  count: number;
90
91
  provider?: undefined;
91
- found?: undefined;
92
92
  registered?: undefined;
93
93
  label?: undefined;
94
94
  message?: undefined;
95
95
  } | {
96
96
  id?: undefined;
97
97
  deleted?: undefined;
98
- count?: undefined;
99
98
  providers?: undefined;
99
+ count?: undefined;
100
100
  found: boolean;
101
101
  provider: import("../custom-registry.js").CustomProviderConfig;
102
102
  registered?: undefined;
@@ -104,8 +104,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
104
104
  message?: undefined;
105
105
  } | {
106
106
  deleted?: undefined;
107
- count?: undefined;
108
107
  providers?: undefined;
108
+ count?: undefined;
109
109
  provider?: undefined;
110
110
  found: boolean;
111
111
  id: string;
@@ -113,10 +113,10 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
113
113
  label?: undefined;
114
114
  message?: undefined;
115
115
  } | {
116
- count?: undefined;
116
+ found?: undefined;
117
117
  providers?: undefined;
118
+ count?: undefined;
118
119
  provider?: undefined;
119
- found?: undefined;
120
120
  deleted: boolean;
121
121
  id: string;
122
122
  registered?: undefined;
@@ -124,10 +124,10 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
124
124
  message?: undefined;
125
125
  } | {
126
126
  deleted?: undefined;
127
- count?: undefined;
127
+ found?: undefined;
128
128
  providers?: undefined;
129
+ count?: undefined;
129
130
  provider?: undefined;
130
- found?: undefined;
131
131
  registered: boolean;
132
132
  id: string;
133
133
  label: string;
@@ -311,10 +311,10 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
311
311
  notes?: string | undefined;
312
312
  scope?: "org" | "user" | undefined;
313
313
  }, {
314
- message?: undefined;
315
314
  id?: undefined;
316
315
  provider?: undefined;
317
316
  deleted?: undefined;
317
+ found?: undefined;
318
318
  providers: {
319
319
  id: string;
320
320
  label: string;
@@ -325,45 +325,45 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
325
325
  updatedAt: number;
326
326
  }[];
327
327
  count: number;
328
- found?: undefined;
329
328
  registered?: undefined;
330
329
  label?: undefined;
331
- } | {
332
330
  message?: undefined;
333
- count?: undefined;
331
+ } | {
334
332
  id?: undefined;
335
333
  deleted?: undefined;
334
+ count?: undefined;
336
335
  providers?: undefined;
337
336
  found: boolean;
338
337
  provider: import("../custom-registry.js").CustomProviderConfig;
339
338
  registered?: undefined;
340
339
  label?: undefined;
341
- } | {
342
340
  message?: undefined;
343
- count?: undefined;
341
+ } | {
344
342
  provider?: undefined;
345
343
  deleted?: undefined;
344
+ count?: undefined;
346
345
  providers?: undefined;
347
346
  found: boolean;
348
347
  id: string;
349
348
  registered?: undefined;
350
349
  label?: undefined;
351
- } | {
352
350
  message?: undefined;
353
- count?: undefined;
351
+ } | {
354
352
  provider?: undefined;
355
- providers?: undefined;
356
353
  found?: undefined;
354
+ count?: undefined;
355
+ providers?: undefined;
357
356
  deleted: boolean;
358
357
  id: string;
359
358
  registered?: undefined;
360
359
  label?: undefined;
360
+ message?: undefined;
361
361
  } | {
362
- count?: undefined;
363
362
  provider?: undefined;
364
363
  deleted?: undefined;
365
- providers?: undefined;
366
364
  found?: undefined;
365
+ count?: undefined;
366
+ providers?: undefined;
367
367
  registered: boolean;
368
368
  id: string;
369
369
  label: string;
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
48
48
  }>;
49
49
  /** DELETE /_agent-native/resources/:id — delete a resource */
50
50
  export declare function handleDeleteResource(event: any): Promise<{
51
- ok?: undefined;
52
51
  error: string;
52
+ ok?: undefined;
53
53
  } | {
54
54
  error?: undefined;
55
55
  ok: boolean;
@@ -34,16 +34,16 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
34
34
  /** POST /_agent-native/secrets/:key — write a secret. */
35
35
  export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
36
36
  error: string;
37
- ok?: undefined;
38
37
  status?: undefined;
38
+ ok?: undefined;
39
39
  } | {
40
40
  ok: boolean;
41
41
  status: string;
42
42
  error?: undefined;
43
43
  } | {
44
- ok?: undefined;
45
44
  error: string;
46
45
  removed?: undefined;
46
+ ok?: undefined;
47
47
  } | {
48
48
  ok: boolean;
49
49
  removed: boolean;
@@ -54,9 +54,9 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
54
54
  * or the current stored value without changing anything.
55
55
  */
56
56
  export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
57
- ok?: undefined;
58
57
  error: string;
59
58
  note?: undefined;
59
+ ok?: undefined;
60
60
  } | {
61
61
  ok: boolean;
62
62
  note?: undefined;
@@ -7,7 +7,7 @@ import { readBody } from "../server/h3-helpers.js";
7
7
  import { EMBED_TARGET_HEADER } from "../shared/embed-auth.js";
8
8
  import { isMcpEmbedCorsOrigin, MCP_EMBED_CORS_ALLOW_HEADERS, shouldAllowMcpEmbedCredentials, } from "../shared/mcp-embed-headers.js";
9
9
  import { actionCallIsReadOnly, notifyActionChange } from "./action-change.js";
10
- import { seedAgentRunOwnerContext, } from "./agent-run-context.js";
10
+ import { readBrowserSessionIdHeader, seedAgentRunOwnerContext, } from "./agent-run-context.js";
11
11
  import { captureError } from "./capture-error.js";
12
12
  import { getAllowedCorsOrigin as resolveAllowedCorsOrigin, readCorsAllowedOrigins, } from "./cors-origins.js";
13
13
  import { resolveEmbedSessionFromRequest, resolvedEmbedCapabilityScope, } from "./embed-session.js";
@@ -158,7 +158,7 @@ function handleOptionsRequest(event) {
158
158
  }
159
159
  setResponseHeader(event, "Access-Control-Allow-Methods", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS");
160
160
  setResponseHeader(event, "Access-Control-Allow-Headers", cors.credentials
161
- ? `Content-Type,Authorization,X-Requested-With,X-Request-Source,X-Agent-Native-CSRF,X-User-Timezone,X-Agent-Native-Tool-Bridge,X-Agent-Native-Tool-Id,X-Agent-Native-Frontend,X-Agent-Native-Client-Compatibility,X-Agent-Native-Build-Id,${EMBED_TARGET_HEADER}`
161
+ ? `Content-Type,Authorization,X-Requested-With,X-Request-Source,X-Agent-Native-CSRF,X-User-Timezone,X-Agent-Native-Session-Id,X-Agent-Native-Tool-Bridge,X-Agent-Native-Tool-Id,X-Agent-Native-Frontend,X-Agent-Native-Client-Compatibility,X-Agent-Native-Build-Id,${EMBED_TARGET_HEADER}`
162
162
  : `${MCP_EMBED_CORS_ALLOW_HEADERS},X-Agent-Native-Tool-Bridge,X-Agent-Native-Tool-Id,X-Agent-Native-Frontend,X-Agent-Native-Client-Compatibility,X-Agent-Native-Build-Id`);
163
163
  }
164
164
  setResponseStatus(event, 204);
@@ -350,12 +350,14 @@ export function mountActionRoutes(nitroApp, actions, options) {
350
350
  : undefined;
351
351
  }
352
352
  const timezone = readTimezoneHeader(event);
353
+ const browserSessionId = readBrowserSessionIdHeader(event);
353
354
  return runWithRequestContext({
354
355
  userEmail,
355
356
  userName,
356
357
  orgId,
357
358
  authCapability,
358
359
  timezone,
360
+ browserSessionId,
359
361
  requestOrigin: getRequestURL(event).origin,
360
362
  // Captured here because this is the last layer that still holds
361
363
  // the h3 event; everything below reads it off the request store.
@@ -41,6 +41,7 @@ import { captureError } from "./capture-error.js";
41
41
  import { getH3App, markDefaultPluginProvided, trackPluginInit, } from "./framework-request-handler.js";
42
42
  import { getOrigin } from "./google-oauth.js";
43
43
  import { readBody } from "./h3-helpers.js";
44
+ import { startIntervalJob } from "./interval-job.js";
44
45
  import { getModelFamilyOverlay } from "./prompts/index.js";
45
46
  import { mountRealtimeVoiceRoutes } from "./realtime-voice.js";
46
47
  import { runWithRequestContext, getRequestContext, getRequestOrgId, getRequestUserEmail, getRequestRunContext, ensureRequestRunContext, } from "./request-context.js";
@@ -4966,35 +4967,31 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4966
4967
  setTimeout(() => {
4967
4968
  (async () => {
4968
4969
  const { UNCLAIMED_BACKGROUND_RUN_FAST_SWEEP_MS } = await import("../agent/run-store.js");
4969
- setInterval(() => {
4970
- (async () => {
4971
- const { listUnclaimedBackgroundRunRows, shouldRedispatchUnclaimedBackgroundRun, reapAllStaleRuns, } = await import("../agent/run-store.js");
4972
- // The unclaimed-background sweep below only matches
4973
- // dispatch_mode='background' — handoffs a worker never
4974
- // claimed. Once a worker CLAIMS a row nothing periodic looked
4975
- // at it again, so a dead producer was only reaped when some
4976
- // client request path or an unrelated run's cleanup happened
4977
- // to notice (prod: 24 minutes after the last heartbeat,
4978
- // against a 45s window). `reapAllStaleRuns` is per-row,
4979
- // idempotent, re-checks staleness at UPDATE time and honours
4980
- // the in-flight grace, so it is safe on this cadence.
4981
- await reapAllStaleRuns().catch(() => { });
4982
- let rows;
4983
- try {
4984
- rows = await listUnclaimedBackgroundRunRows();
4985
- }
4986
- catch {
4987
- return; // Table may not exist yet on first boot
4988
- }
4989
- for (const row of rows) {
4990
- if (!shouldRedispatchUnclaimedBackgroundRun(row))
4991
- continue;
4992
- await attemptUnclaimedBackgroundRunRedispatch(row).catch(() => { });
4993
- }
4994
- })().catch(() => {
4995
- // best-effort — never break the server
4996
- });
4997
- }, UNCLAIMED_BACKGROUND_RUN_FAST_SWEEP_MS);
4970
+ startIntervalJob(async () => {
4971
+ const { listUnclaimedBackgroundRunRows, shouldRedispatchUnclaimedBackgroundRun, reapAllStaleRuns, } = await import("../agent/run-store.js");
4972
+ // The unclaimed-background sweep below only matches
4973
+ // dispatch_mode='background' — handoffs a worker never
4974
+ // claimed. Once a worker CLAIMS a row nothing periodic looked
4975
+ // at it again, so a dead producer was only reaped when some
4976
+ // client request path or an unrelated run's cleanup happened
4977
+ // to notice (prod: 24 minutes after the last heartbeat,
4978
+ // against a 45s window). `reapAllStaleRuns` is per-row,
4979
+ // idempotent, re-checks staleness at UPDATE time and honours
4980
+ // the in-flight grace, so it is safe on this cadence.
4981
+ await reapAllStaleRuns().catch(() => { });
4982
+ let rows;
4983
+ try {
4984
+ rows = await listUnclaimedBackgroundRunRows();
4985
+ }
4986
+ catch {
4987
+ return; // Table may not exist yet on first boot
4988
+ }
4989
+ for (const row of rows) {
4990
+ if (!shouldRedispatchUnclaimedBackgroundRun(row))
4991
+ continue;
4992
+ await attemptUnclaimedBackgroundRunRedispatch(row).catch(() => { });
4993
+ }
4994
+ }, { intervalMs: UNCLAIMED_BACKGROUND_RUN_FAST_SWEEP_MS });
4998
4995
  })().catch(() => {
4999
4996
  // best-effort — if run-store fails to load, the slow sweep below
5000
4997
  // still provides eventual (loud) recovery.
@@ -27,10 +27,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
27
27
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
28
28
  error: any;
29
29
  } | {
30
- error?: undefined;
31
30
  ok: boolean;
32
31
  key: string;
33
32
  baseUrlKey?: string;
34
33
  scope: AgentEngineApiKeyScope;
34
+ error?: undefined;
35
35
  }>>;
36
36
  export {};
@@ -14,9 +14,11 @@ export declare function readAgentRunTimezone(event: H3Event): string | undefined
14
14
  *
15
15
  * Emitted as PostHog's `$session_id` on the run's `$ai_*` events so an agent
16
16
  * trace joins to the session replay it happened in. Distinct from
17
- * `$ai_session_id`, which is the conversation thread.
17
+ * `$ai_session_id`, which is the conversation thread. Read by both the agent
18
+ * run path and the HTTP action route, so one visit correlates across the UI's
19
+ * action calls and the agent's.
18
20
  */
19
- export declare function readAgentRunBrowserSessionId(event: H3Event): string | undefined;
21
+ export declare function readBrowserSessionIdHeader(event: H3Event): string | undefined;
20
22
  export declare function seedAgentRunOwnerContext(event: H3Event, ownerContext: AgentRunOwnerContext): AgentRunOwnerContext;
21
23
  export declare function seedBackgroundAgentRunOwnerContext(event: H3Event, runId: string): Promise<AgentRunOwnerContext | null>;
22
24
  export declare function resolveAgentRunOwnerContext(event: H3Event, options?: {
@@ -49,9 +49,11 @@ export function readAgentRunTimezone(event) {
49
49
  *
50
50
  * Emitted as PostHog's `$session_id` on the run's `$ai_*` events so an agent
51
51
  * trace joins to the session replay it happened in. Distinct from
52
- * `$ai_session_id`, which is the conversation thread.
52
+ * `$ai_session_id`, which is the conversation thread. Read by both the agent
53
+ * run path and the HTTP action route, so one visit correlates across the UI's
54
+ * action calls and the agent's.
53
55
  */
54
- export function readAgentRunBrowserSessionId(event) {
56
+ export function readBrowserSessionIdHeader(event) {
55
57
  const raw = readHeaderValue(event, "x-agent-native-session-id");
56
58
  const value = Array.isArray(raw) ? raw[0] : raw;
57
59
  return typeof value === "string" &&
@@ -140,7 +142,7 @@ export async function resolveAgentRunOrgId(options) {
140
142
  export async function resolveAgentRunRequestContext(options) {
141
143
  const orgId = await resolveAgentRunOrgId(options);
142
144
  const timezone = readAgentRunTimezone(options.event);
143
- const browserSessionId = readAgentRunBrowserSessionId(options.event);
145
+ const browserSessionId = readBrowserSessionIdHeader(options.event);
144
146
  const waitUntil = requestWaitUntil(options.event);
145
147
  const run = {
146
148
  ...(options.isBackgroundWorker ? { isBackgroundWorker: true } : {}),
@@ -33,6 +33,7 @@ import { registerBuiltinProviders } from "../tracking/providers.js";
33
33
  import { validateTrackPayload } from "../tracking/route.js";
34
34
  import { createAutomationsHandler } from "../triggers/routes.js";
35
35
  import { createAgentEngineApiKeyHandler } from "./agent-engine-api-key-route.js";
36
+ import { readBrowserSessionIdHeader } from "./agent-run-context.js";
36
37
  import { getConfiguredAppBasePath, stripAppBasePath } from "./app-base-path.js";
37
38
  import { getAppName } from "./app-name.js";
38
39
  import { getSession } from "./auth.js";
@@ -2544,6 +2545,7 @@ export function createCoreRoutesPlugin(options = {}) {
2544
2545
  try {
2545
2546
  track(validation.name, properties, {
2546
2547
  userId: userEmail,
2548
+ sessionId: readBrowserSessionIdHeader(event),
2547
2549
  });
2548
2550
  }
2549
2551
  catch {
@@ -1,4 +1,5 @@
1
1
  export { createServer, type CreateServerOptions, type EnvKeyConfig, } from "./create-server.js";
2
+ export { startIntervalJob, type IntervalJobOptions, type IntervalJobHandle, } from "./interval-job.js";
2
3
  export { AGENT_BACKGROUND_PROCESSOR_FIELD, AGENT_BACKGROUND_PROCESSOR_ROUTE, AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD, dispatchPathTargetsNetlifyBackgroundFunction, isInBackgroundFunctionRuntime, resolveDurableBackgroundDispatchPath, } from "../agent/durable-background.js";
3
4
  export { readBody, readBodyWithSizeLimit, streamFile, DEFAULT_CHAT_MAX_BODY_BYTES, DEFAULT_UPLOAD_MAX_FILE_BYTES, MAX_CHAT_ATTACHMENTS_PER_MESSAGE, isAllowedUploadMimeType, } from "./h3-helpers.js";
4
5
  export { buildDeepLink, toAbsoluteOpenUrl, toDesktopOpenUrl, toVsCodeOpenUrl, OPEN_ROUTE_SUBPATH, DESKTOP_OPEN_URL, VSCODE_OPEN_URL, type DeepLinkInput, } from "./deep-link.js";
@@ -1,4 +1,5 @@
1
1
  export { createServer, } from "./create-server.js";
2
+ export { startIntervalJob, } from "./interval-job.js";
2
3
  export { AGENT_BACKGROUND_PROCESSOR_FIELD, AGENT_BACKGROUND_PROCESSOR_ROUTE, AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD, dispatchPathTargetsNetlifyBackgroundFunction, isInBackgroundFunctionRuntime, resolveDurableBackgroundDispatchPath, } from "../agent/durable-background.js";
3
4
  export { readBody, readBodyWithSizeLimit, streamFile, DEFAULT_CHAT_MAX_BODY_BYTES, DEFAULT_UPLOAD_MAX_FILE_BYTES, MAX_CHAT_ATTACHMENTS_PER_MESSAGE, isAllowedUploadMimeType, } from "./h3-helpers.js";
4
5
  export { buildDeepLink, toAbsoluteOpenUrl, toDesktopOpenUrl, toVsCodeOpenUrl, OPEN_ROUTE_SUBPATH, DESKTOP_OPEN_URL, VSCODE_OPEN_URL, } from "./deep-link.js";
@@ -0,0 +1,25 @@
1
+ export interface IntervalJobOptions {
2
+ /** Delay in ms between runs. */
3
+ intervalMs: number;
4
+ /** Per-run timeout. Default: `Math.max(timeoutFloorMs, intervalMs * 4)`. */
5
+ timeoutMs?: number;
6
+ /** Floor used by the default `timeoutMs`. Default: 10_000. */
7
+ timeoutFloorMs?: number;
8
+ /** Called when a run throws or times out. Default: swallow (a background job shouldn't crash the host process). */
9
+ onError?: (err: unknown) => void;
10
+ /** Run immediately on start. Default: true. */
11
+ leading?: boolean;
12
+ }
13
+ export interface IntervalJobHandle {
14
+ stop(): void;
15
+ }
16
+ /**
17
+ * Run a background job on a recurring interval without hand-rolling a
18
+ * `running` overlap guard or a request timeout — see `createPollEngine` for
19
+ * the underlying guarantees. `runOnce` keeps its normal exported signature
20
+ * (serverless schedulers can still call it directly, bypassing the loop);
21
+ * this only replaces the `setInterval` + module-level flag wiring around it.
22
+ * The internal timer is `.unref()`'d so it never keeps the process alive on
23
+ * its own.
24
+ */
25
+ export declare function startIntervalJob(runOnce: (signal: AbortSignal) => Promise<void>, options: IntervalJobOptions): IntervalJobHandle;
@@ -0,0 +1,19 @@
1
+ import { createPollEngine } from "../shared/poll-engine.js";
2
+ /**
3
+ * Run a background job on a recurring interval without hand-rolling a
4
+ * `running` overlap guard or a request timeout — see `createPollEngine` for
5
+ * the underlying guarantees. `runOnce` keeps its normal exported signature
6
+ * (serverless schedulers can still call it directly, bypassing the loop);
7
+ * this only replaces the `setInterval` + module-level flag wiring around it.
8
+ * The internal timer is `.unref()`'d so it never keeps the process alive on
9
+ * its own.
10
+ */
11
+ export function startIntervalJob(runOnce, options) {
12
+ const engine = createPollEngine(runOnce, options);
13
+ engine.start();
14
+ return {
15
+ stop() {
16
+ engine.stop();
17
+ },
18
+ };
19
+ }
@@ -9,6 +9,7 @@ export { llmConnectionTrackingProperties, normalizeLlmConnection, type LlmConnec
9
9
  export { DISPATCH_WORKSPACE_ROOT_REDIRECTS, RESERVED_WORKSPACE_APP_IDS, assertValidWorkspaceAppId, getWorkspaceAppIdValidationError, isValidWorkspaceAppIdFormat, } from "./workspace-app-id.js";
10
10
  export { DEFAULT_WORKSPACE_APP_AUDIENCE, WORKSPACE_APP_AUDIENCES, normalizeWorkspaceAppAudience, normalizeWorkspaceAppPathList, workspaceAppAudienceFromEnv, workspaceAppAudienceFromPackageJson, workspaceAppRouteAccessFromEnv, workspaceAppRouteAccessFromPackageJson, type WorkspaceAppRouteAccess, type WorkspaceAppRouteAccessFromConfig, type WorkspaceAppAudience, } from "./workspace-app-audience.js";
11
11
  export { AGENT_NATIVE_OPEN_PATH, AGENT_SIDEBAR_QUERY_PARAM, AGENT_SIDEBAR_QUERY_VALUE_CLOSED, isAgentNativeOpenDeepLink, withCollapsedAgentSidebarParam, } from "./agent-sidebar-url.js";
12
+ export { createPollEngine, type PollEngineOptions, type PollEngineHandle, } from "./poll-engine.js";
12
13
  export { AGENT_NATIVE_DEFAULT_SOCIAL_IMAGE, AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER, AGENT_NATIVE_SOCIAL_IMAGE_ALT, AGENT_NATIVE_SOCIAL_IMAGE_HEIGHT, AGENT_NATIVE_SOCIAL_IMAGE_PATH, AGENT_NATIVE_SOCIAL_IMAGE_TYPE, AGENT_NATIVE_SOCIAL_IMAGE_WIDTH, defaultSocialImageMeta, withAgentNativeSocialImageCacheBuster, withDefaultSocialImage, type SocialMetaDescriptor, } from "./social-meta.js";
13
14
  export { EMBED_MODE_QUERY_PARAM, EMBED_SESSION_COOKIE, EMBED_START_PATH, EMBED_TOKEN_QUERY_PARAM, } from "./embed-auth.js";
14
15
  export { AGENT_ACCESS_PARAM, DEFAULT_AGENT_ACCESS_TTL_SECONDS, appendAgentAccessParam, buildAgentAccessApiUrl, buildAgentAccessUrl, normalizeAgentAccessBasePath, normalizeAgentAccessOrigin, scopedAgentAccessResourceId, toAgentAccessUrl, type AgentAccessApiUrlOptions, type AgentAccessResourceScope, type AgentAccessUrlOptions, } from "./agent-access.js";
@@ -9,6 +9,7 @@ export { llmConnectionTrackingProperties, normalizeLlmConnection, } from "./llm-
9
9
  export { DISPATCH_WORKSPACE_ROOT_REDIRECTS, RESERVED_WORKSPACE_APP_IDS, assertValidWorkspaceAppId, getWorkspaceAppIdValidationError, isValidWorkspaceAppIdFormat, } from "./workspace-app-id.js";
10
10
  export { DEFAULT_WORKSPACE_APP_AUDIENCE, WORKSPACE_APP_AUDIENCES, normalizeWorkspaceAppAudience, normalizeWorkspaceAppPathList, workspaceAppAudienceFromEnv, workspaceAppAudienceFromPackageJson, workspaceAppRouteAccessFromEnv, workspaceAppRouteAccessFromPackageJson, } from "./workspace-app-audience.js";
11
11
  export { AGENT_NATIVE_OPEN_PATH, AGENT_SIDEBAR_QUERY_PARAM, AGENT_SIDEBAR_QUERY_VALUE_CLOSED, isAgentNativeOpenDeepLink, withCollapsedAgentSidebarParam, } from "./agent-sidebar-url.js";
12
+ export { createPollEngine, } from "./poll-engine.js";
12
13
  export { AGENT_NATIVE_DEFAULT_SOCIAL_IMAGE, AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER, AGENT_NATIVE_SOCIAL_IMAGE_ALT, AGENT_NATIVE_SOCIAL_IMAGE_HEIGHT, AGENT_NATIVE_SOCIAL_IMAGE_PATH, AGENT_NATIVE_SOCIAL_IMAGE_TYPE, AGENT_NATIVE_SOCIAL_IMAGE_WIDTH, defaultSocialImageMeta, withAgentNativeSocialImageCacheBuster, withDefaultSocialImage, } from "./social-meta.js";
13
14
  export { EMBED_MODE_QUERY_PARAM, EMBED_SESSION_COOKIE, EMBED_START_PATH, EMBED_TOKEN_QUERY_PARAM, } from "./embed-auth.js";
14
15
  export { AGENT_ACCESS_PARAM, DEFAULT_AGENT_ACCESS_TTL_SECONDS, appendAgentAccessParam, buildAgentAccessApiUrl, buildAgentAccessUrl, normalizeAgentAccessBasePath, normalizeAgentAccessOrigin, scopedAgentAccessResourceId, toAgentAccessUrl, } from "./agent-access.js";
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Shared recurring-poll engine used by both `usePollLoop` (client) and
3
+ * `startIntervalJob` (server) so every polling loop in the codebase gets the
4
+ * same three properties for free instead of hand-rolling them per call site:
5
+ *
6
+ * 1. Never overlaps: the next attempt is scheduled via `setTimeout` only
7
+ * after the current one settles, not on a fixed `setInterval` cadence.
8
+ * This also avoids the latency `setInterval` + skip-if-busy introduces —
9
+ * a skipped tick there means waiting a full extra cadence before the next
10
+ * retry; here the gap after completion is always exactly `intervalMs`.
11
+ * 2. Never stalls silently: every attempt is bounded by a timeout (paired
12
+ * with an `AbortController` so a `fetch` can actually cancel, not just be
13
+ * abandoned), and that timeout reports through `onError` the moment it
14
+ * fires. An attempt that ignores its `signal` keeps holding the loop —
15
+ * reported, not hidden — because releasing the slot to the next attempt
16
+ * would trade a visible stall for a silent overlap.
17
+ * 3. Never double-fires: a single internal in-flight flag, owned by this
18
+ * engine rather than duplicated in every caller.
19
+ */
20
+ export interface PollEngineOptions {
21
+ /** Delay in ms before the next attempt. Evaluated fresh before each schedule. */
22
+ intervalMs: number | (() => number);
23
+ /** Per-attempt timeout. Default: `Math.max(timeoutFloorMs, intervalMs * 4)`. */
24
+ timeoutMs?: number | (() => number);
25
+ /** Floor used by the default `timeoutMs`. Default: 10_000. */
26
+ timeoutFloorMs?: number;
27
+ /** Called when an attempt throws or times out. Default: swallow. */
28
+ onError?: (err: unknown) => void;
29
+ /** Run the first attempt immediately on `start()`. Default: true. */
30
+ leading?: boolean;
31
+ }
32
+ export interface PollEngineHandle {
33
+ /** Arm the loop. No-op if already running. */
34
+ start(): void;
35
+ /** Halt the loop and abort any in-flight attempt; its eventual settlement is a no-op. */
36
+ stop(): void;
37
+ /** Cancel the pending wait and run now. No-op if not running or an attempt is already in flight. */
38
+ pollNow(): void;
39
+ /**
40
+ * Re-arm the pending wait using a freshly resolved `intervalMs`, without
41
+ * running an attempt. Use this when `intervalMs` is a function whose
42
+ * result can change externally (e.g. tab visibility) — otherwise a
43
+ * change only takes effect the next time an attempt completes. No-op if
44
+ * there's no pending wait (not running, or an attempt is in flight).
45
+ */
46
+ reschedule(): void;
47
+ readonly isRunning: boolean;
48
+ }
49
+ export declare function createPollEngine(attempt: (signal: AbortSignal) => Promise<void>, options: PollEngineOptions): PollEngineHandle;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Shared recurring-poll engine used by both `usePollLoop` (client) and
3
+ * `startIntervalJob` (server) so every polling loop in the codebase gets the
4
+ * same three properties for free instead of hand-rolling them per call site:
5
+ *
6
+ * 1. Never overlaps: the next attempt is scheduled via `setTimeout` only
7
+ * after the current one settles, not on a fixed `setInterval` cadence.
8
+ * This also avoids the latency `setInterval` + skip-if-busy introduces —
9
+ * a skipped tick there means waiting a full extra cadence before the next
10
+ * retry; here the gap after completion is always exactly `intervalMs`.
11
+ * 2. Never stalls silently: every attempt is bounded by a timeout (paired
12
+ * with an `AbortController` so a `fetch` can actually cancel, not just be
13
+ * abandoned), and that timeout reports through `onError` the moment it
14
+ * fires. An attempt that ignores its `signal` keeps holding the loop —
15
+ * reported, not hidden — because releasing the slot to the next attempt
16
+ * would trade a visible stall for a silent overlap.
17
+ * 3. Never double-fires: a single internal in-flight flag, owned by this
18
+ * engine rather than duplicated in every caller.
19
+ */
20
+ const DEFAULT_TIMEOUT_FLOOR_MS = 10_000;
21
+ function resolve(value) {
22
+ return typeof value === "function" ? value() : value;
23
+ }
24
+ function maybeUnref(timer) {
25
+ if (timer &&
26
+ typeof timer === "object" &&
27
+ "unref" in timer &&
28
+ typeof timer.unref === "function") {
29
+ timer.unref();
30
+ }
31
+ }
32
+ export function createPollEngine(attempt, options) {
33
+ const timeoutFloorMs = options.timeoutFloorMs ?? DEFAULT_TIMEOUT_FLOOR_MS;
34
+ const getTimeoutMs = () => options.timeoutMs != null
35
+ ? resolve(options.timeoutMs)
36
+ : Math.max(timeoutFloorMs, resolve(options.intervalMs) * 4);
37
+ const onError = options.onError ?? (() => { });
38
+ const leading = options.leading ?? true;
39
+ let generation = 0;
40
+ let running = false;
41
+ let inFlight = false;
42
+ let timer = null;
43
+ let activeController = null;
44
+ function clearTimer() {
45
+ if (timer != null) {
46
+ clearTimeout(timer);
47
+ timer = null;
48
+ }
49
+ }
50
+ function schedule(gen) {
51
+ clearTimer();
52
+ timer = setTimeout(() => {
53
+ timer = null;
54
+ void tick(gen);
55
+ }, resolve(options.intervalMs));
56
+ maybeUnref(timer);
57
+ }
58
+ async function tick(gen) {
59
+ if (gen !== generation || !running)
60
+ return;
61
+ if (inFlight) {
62
+ // An attempt from a superseded generation is still settling — stop()
63
+ // aborted it but the caller has not returned yet. Its own `finally`
64
+ // sees a stale generation and will not reschedule, so re-arming here
65
+ // is what keeps a stop()/start() cycle (tab hidden then visible) from
66
+ // leaving the engine alive-but-timerless, i.e. permanently dead.
67
+ schedule(gen);
68
+ return;
69
+ }
70
+ inFlight = true;
71
+ const controller = new AbortController();
72
+ activeController = controller;
73
+ const timeoutMs = getTimeoutMs();
74
+ const abortTimer = setTimeout(() => controller.abort(), timeoutMs);
75
+ maybeUnref(abortTimer);
76
+ let reported = false;
77
+ const report = (err) => {
78
+ if (reported)
79
+ return;
80
+ reported = true;
81
+ onError(err);
82
+ };
83
+ // The attempt is retained separately from the timeout race below. The
84
+ // timeout reports a slow attempt immediately, but the in-flight slot is
85
+ // only released once the attempt itself settles: releasing on the
86
+ // timeout would let the next tick start while an attempt that ignores
87
+ // `signal` is still doing work, which is the overlap (duplicate external
88
+ // requests, racing writes) this engine exists to prevent. An attempt that
89
+ // never settles blocks further attempts by design — `onError` has already
90
+ // fired, so it is loud rather than silent.
91
+ const settled = Promise.resolve()
92
+ .then(() => attempt(controller.signal))
93
+ .then(() => { }, (err) => report(err));
94
+ try {
95
+ await Promise.race([
96
+ settled,
97
+ new Promise((_, reject) => {
98
+ controller.signal.addEventListener("abort", () => reject(new Error(`poll attempt timed out after ${timeoutMs}ms`)), { once: true });
99
+ }),
100
+ ]);
101
+ }
102
+ catch (err) {
103
+ report(err);
104
+ }
105
+ finally {
106
+ await settled;
107
+ clearTimeout(abortTimer);
108
+ if (activeController === controller)
109
+ activeController = null;
110
+ inFlight = false;
111
+ if (gen === generation && running)
112
+ schedule(gen);
113
+ }
114
+ }
115
+ return {
116
+ start() {
117
+ if (running)
118
+ return;
119
+ running = true;
120
+ generation++;
121
+ const gen = generation;
122
+ if (leading)
123
+ void tick(gen);
124
+ else
125
+ schedule(gen);
126
+ },
127
+ stop() {
128
+ running = false;
129
+ generation++;
130
+ clearTimer();
131
+ activeController?.abort();
132
+ },
133
+ pollNow() {
134
+ if (!running || inFlight)
135
+ return;
136
+ clearTimer();
137
+ void tick(generation);
138
+ },
139
+ reschedule() {
140
+ if (!running || inFlight || timer == null)
141
+ return;
142
+ schedule(generation);
143
+ },
144
+ get isRunning() {
145
+ return running;
146
+ },
147
+ };
148
+ }
@@ -29,6 +29,11 @@ Templates use `app/i18n/`:
29
29
  Use BCP-47 filenames from the supported set: `en-US`, `zh-CN`, `es-ES`,
30
30
  `fr-FR`, `de-DE`, `ja-JP`, `ko-KR`, `pt-BR`, `hi-IN`, `ar-SA`.
31
31
 
32
+ If an app only ships translations for a subset of that set, set
33
+ `supportedLocales` on the `AgentNativeI18nCatalog` to that subset. Without it,
34
+ `<LanguagePicker />` lists every framework-supported locale regardless of
35
+ whether `loadMessages` can actually resolve it.
36
+
32
37
  ## UI Pattern
33
38
 
34
39
  - Wrap apps with `AppProviders i18n={{ catalog: i18nCatalog }}`.
@@ -29,6 +29,11 @@ Templates use `app/i18n/`:
29
29
  Use BCP-47 filenames from the supported set: `en-US`, `zh-CN`, `es-ES`,
30
30
  `fr-FR`, `de-DE`, `ja-JP`, `ko-KR`, `pt-BR`, `hi-IN`, `ar-SA`.
31
31
 
32
+ If an app only ships translations for a subset of that set, set
33
+ `supportedLocales` on the `AgentNativeI18nCatalog` to that subset. Without it,
34
+ `<LanguagePicker />` lists every framework-supported locale regardless of
35
+ whether `loadMessages` can actually resolve it.
36
+
32
37
  ## UI Pattern
33
38
 
34
39
  - Wrap apps with `AppProviders i18n={{ catalog: i18nCatalog }}`.