@agent-native/core 0.79.6 → 0.79.9

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 (97) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/docs/content/locales/zh-TW/durable-background-runs.md +74 -0
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/agent/production-agent.ts +0 -93
  6. package/corpus/core/src/agent/run-manager.ts +9 -1
  7. package/corpus/core/src/client/agent-chat-adapter.ts +24 -16
  8. package/corpus/core/src/client/session-replay.ts +67 -13
  9. package/corpus/core/src/onboarding/default-steps.ts +3 -3
  10. package/corpus/core/src/server/agent-chat-plugin.ts +82 -162
  11. package/corpus/core/src/server/agent-run-context.ts +204 -0
  12. package/corpus/core/src/server/credential-provider.ts +20 -4
  13. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +14 -4
  14. package/corpus/templates/analytics/changelog/2026-06-26-session-replay-uploads-now-accept-compressed-payloads-so-lar.md +6 -0
  15. package/corpus/templates/analytics/changelog/2026-06-26-sql-chart-tooltips-now-stay-stable-while-moving-across-dense.md +6 -0
  16. package/corpus/templates/analytics/server/handlers/session-replay.ts +56 -8
  17. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +1 -1
  18. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +44 -16
  19. package/corpus/templates/clips/app/i18n/ar-SA.ts +12 -9
  20. package/corpus/templates/clips/app/i18n/de-DE.ts +12 -9
  21. package/corpus/templates/clips/app/i18n/en-US.ts +19 -16
  22. package/corpus/templates/clips/app/i18n/es-ES.ts +12 -9
  23. package/corpus/templates/clips/app/i18n/fr-FR.ts +12 -9
  24. package/corpus/templates/clips/app/i18n/hi-IN.ts +13 -9
  25. package/corpus/templates/clips/app/i18n/ja-JP.ts +12 -9
  26. package/corpus/templates/clips/app/i18n/ko-KR.ts +12 -9
  27. package/corpus/templates/clips/app/i18n/pt-BR.ts +12 -9
  28. package/corpus/templates/clips/app/i18n/zh-CN.ts +19 -14
  29. package/corpus/templates/clips/app/i18n/zh-TW.ts +19 -14
  30. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +7 -0
  31. package/corpus/templates/clips/app/routes/record.tsx +2 -2
  32. package/corpus/templates/clips/changelog/2026-06-26-desktop-recordings-recover-more-reliably-when-macos-reports-.md +6 -0
  33. package/corpus/templates/clips/changelog/2026-06-26-slack-unfurls-now-recognize-recording-dashboard-links-pasted.md +6 -0
  34. package/corpus/templates/clips/changelog/2026-06-26-storage-setup-now-explains-why-clips-needs-connected-storage.md +6 -0
  35. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  36. package/corpus/templates/clips/chrome-extension/src/background.ts +24 -3
  37. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +28 -2
  38. package/corpus/templates/clips/chrome-extension/src/popup.html +16 -0
  39. package/corpus/templates/clips/chrome-extension/src/popup.ts +59 -7
  40. package/corpus/templates/clips/chrome-extension/src/sentry.ts +28 -1
  41. package/corpus/templates/clips/chrome-extension/src/styles.css +74 -0
  42. package/corpus/templates/clips/desktop/src/app.tsx +85 -18
  43. package/corpus/templates/clips/desktop/src/styles.css +55 -0
  44. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +88 -51
  45. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +12 -3
  46. package/corpus/templates/clips/server/lib/slack-unfurls.ts +5 -2
  47. package/corpus/templates/slides/actions/_app-url.ts +1 -1
  48. package/corpus/templates/slides/changelog/2026-06-26-deck-links-now-use-the-workspace-gateway-when-available-so-g.md +6 -0
  49. package/dist/agent/production-agent.d.ts.map +1 -1
  50. package/dist/agent/production-agent.js +0 -79
  51. package/dist/agent/production-agent.js.map +1 -1
  52. package/dist/agent/run-manager.d.ts.map +1 -1
  53. package/dist/agent/run-manager.js +12 -1
  54. package/dist/agent/run-manager.js.map +1 -1
  55. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  56. package/dist/client/agent-chat-adapter.js +15 -8
  57. package/dist/client/agent-chat-adapter.js.map +1 -1
  58. package/dist/client/session-replay.d.ts.map +1 -1
  59. package/dist/client/session-replay.js +55 -14
  60. package/dist/client/session-replay.js.map +1 -1
  61. package/dist/collab/awareness.d.ts +2 -2
  62. package/dist/collab/awareness.d.ts.map +1 -1
  63. package/dist/collab/routes.d.ts +2 -2
  64. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  65. package/dist/notifications/routes.d.ts +1 -1
  66. package/dist/observability/routes.d.ts +7 -7
  67. package/dist/onboarding/default-steps.js +3 -3
  68. package/dist/onboarding/default-steps.js.map +1 -1
  69. package/dist/progress/routes.d.ts +1 -1
  70. package/dist/resources/handlers.d.ts +3 -3
  71. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  72. package/dist/server/agent-chat-plugin.js +69 -154
  73. package/dist/server/agent-chat-plugin.js.map +1 -1
  74. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  75. package/dist/server/agent-run-context.d.ts +35 -0
  76. package/dist/server/agent-run-context.d.ts.map +1 -0
  77. package/dist/server/agent-run-context.js +137 -0
  78. package/dist/server/agent-run-context.js.map +1 -0
  79. package/dist/server/credential-provider.d.ts.map +1 -1
  80. package/dist/server/credential-provider.js +15 -4
  81. package/dist/server/credential-provider.js.map +1 -1
  82. package/dist/server/transcribe-voice.d.ts +1 -1
  83. package/docs/content/locales/zh-TW/durable-background-runs.md +74 -0
  84. package/package.json +1 -1
  85. package/corpus/templates/.retired/README.md +0 -9
  86. package/corpus/templates/.retired/calls/package.json +0 -8
  87. package/corpus/templates/.retired/code/package.json +0 -8
  88. package/corpus/templates/.retired/contracts/package.json +0 -8
  89. package/corpus/templates/.retired/images/package.json +0 -8
  90. package/corpus/templates/.retired/issues/package.json +0 -8
  91. package/corpus/templates/.retired/meeting-notes/package.json +0 -8
  92. package/corpus/templates/.retired/migration/package.json +0 -8
  93. package/corpus/templates/.retired/recruiting/package.json +0 -8
  94. package/corpus/templates/.retired/scheduling/package.json +0 -8
  95. package/corpus/templates/.retired/visual-plans/package.json +0 -8
  96. package/corpus/templates/.retired/voice/package.json +0 -8
  97. package/corpus/templates/.retired/workbench/package.json +0 -8
@@ -147,6 +147,12 @@ import {
147
147
  import { dbExecToolParameters } from "../scripts/db/tool-schemas.js";
148
148
  import { getSetting, putSetting } from "../settings/store.js";
149
149
  import { discoverAgents } from "./agent-discovery.js";
150
+ import {
151
+ resolveAgentRunOwnerContext,
152
+ runWithAgentRunContext,
153
+ seedBackgroundAgentRunOwnerContext,
154
+ type AgentRunOwnerContext,
155
+ } from "./agent-run-context.js";
150
156
  import {
151
157
  AGENT_TEAM_PROCESS_RUN_PATH,
152
158
  getCurrentDelegationDepth,
@@ -159,6 +165,7 @@ import {
159
165
  getBuilderBrowserConnectUrlForOwner,
160
166
  resolveBuilderBranchProjectId,
161
167
  } from "./builder-browser.js";
168
+ import { captureError } from "./capture-error.js";
162
169
  import { captureCliOutput } from "./cli-capture.js";
163
170
  import {
164
171
  getH3App,
@@ -871,14 +878,51 @@ export function assembleA2AFinalResponse(
871
878
  toolResults: readonly A2AToolResultSummary[],
872
879
  options: A2AArtifactResponseOptions & { event?: any } = {},
873
880
  ): { responseText: string; finalText: string } {
874
- const responseText = collectFinalResponseTextFromAgentEvents(events);
881
+ const terminalError = getA2ATerminalErrorEvent(events);
882
+ const responseText = collectFinalResponseTextFromAgentEvents(events, {
883
+ fallbackToPreToolText: !terminalError,
884
+ });
875
885
  const finalText = appendA2AArtifactLinks(responseText, [...toolResults], {
876
886
  baseUrl: options.baseUrl ?? resolveArtifactBaseUrl(options.event),
877
887
  includeReferencedArtifacts: true,
878
888
  });
889
+ if (terminalError && !finalText.trim()) {
890
+ throw new Error(formatA2ATerminalError(terminalError));
891
+ }
879
892
  return { responseText, finalText };
880
893
  }
881
894
 
895
+ function getA2ATerminalErrorEvent(
896
+ events: readonly AgentChatEvent[],
897
+ ): Extract<AgentChatEvent, { type: "error" }> | null {
898
+ for (let i = events.length - 1; i >= 0; i--) {
899
+ const event = events[i];
900
+ if (event.type === "clear") continue;
901
+ if (event.type === "done") return null;
902
+ if (event.type === "error") return event;
903
+ if (event.type === "auto_continue") {
904
+ return {
905
+ type: "error",
906
+ error: `Agent stopped before finishing (${event.reason}).`,
907
+ errorCode: event.reason,
908
+ recoverable: true,
909
+ };
910
+ }
911
+ }
912
+ return null;
913
+ }
914
+
915
+ function formatA2ATerminalError(
916
+ event: Extract<AgentChatEvent, { type: "error" }>,
917
+ ): string {
918
+ const parts = [
919
+ event.error || "Agent failed before producing a final response.",
920
+ event.errorCode ? `code: ${event.errorCode}` : "",
921
+ event.details ? `details: ${event.details}` : "",
922
+ ].filter(Boolean);
923
+ return parts.join("\n");
924
+ }
925
+
882
926
  /**
883
927
  * Creates the `get-framework-context` tool. Returns detailed instructions
884
928
  * for framework capabilities that are summarized in the compact prompt.
@@ -3052,6 +3096,8 @@ In Act mode, if the user asks for generated interactive UI in chat, choose the s
3052
3096
 
3053
3097
  These are **NOT** source-code changes and do **NOT** go through \`connect-builder\`. Extensions are sandboxed mini-apps — no source files are touched, no PR is opened, no build is required. Saved extensions can be edited later via \`update-extension\`.
3054
3098
 
3099
+ If the app exposes native actions or instructions for dashboards, reports, analyses, charts, documents, decks, or other domain artifacts, use those app-native actions first. Choose an extension only when the user explicitly asks for an extension/custom mini-app, or when the app's native artifact format cannot faithfully express the requested interaction.
3100
+
3055
3101
  Keep \`create-extension\` payloads compact enough to finish quickly. For complex extensions, create a useful working v1 first, then call \`update-extension\` with focused edits for refinements instead of trying to assemble one enormous initial tool input.
3056
3102
 
3057
3103
  Generated UI content can use appAction(), appFetch(), dbQuery(), dbExec(), extensionFetch(), extensionData, agentNative.ui.output(value, opts?), and agentNative.chat.send(...)/sendToAgentChat(...). It can receive chat inputs through slotContext/window.onSlotContext. Use agentNative.ui.output for passive current values from knobs, sliders, selections, and controls; it writes application state at \`inline-ui:<extensionId>:output\` scoped to the inline extension id returned by \`render-inline-extension\` or \`show-extension-inline\`. When the user later says "use that value", "apply the current setting", or similar, read it with \`readAppState("inline-ui:<id>:output")\` instead of asking them to send it again. Use agentNative.chat.send for visible submit/apply actions that should put a message into chat. Transient extensionData is browser-local and not agent-readable, synced, promoted, or garbage-collected; use application_state/appFetch, appAction, ui.output, or chat.send for anything the agent or app must observe. Use semantic Tailwind classes like bg-background, text-foreground, bg-primary, border-border, and text-muted-foreground so the UI inherits the parent app theme.
@@ -3079,7 +3125,7 @@ Route by what the request changes, not how it is phrased. Extensions render in t
3079
3125
  | Ambiguous, satisfiable either way (e.g. "give me an unread view") | \`render-inline-extension\` for chat-only, \`create-extension\` for reusable |
3080
3126
  </routing>
3081
3127
 
3082
- Worked examples: "a widget showing unread emails grouped by sender", "a dashboard summarizing my pipeline", "a tracker for my newsletter subscriptions" → \`create-extension\`. "Add an Unread tab to the left navigation", "make the subject lines wrap", "change the inbox grouping logic", "add a field to the compose form" → \`connect-builder\`.`
3128
+ Worked examples: "a widget showing unread emails grouped by sender", "a tracker for my newsletter subscriptions", "a custom kanban board with drag-and-drop rules the app does not have" → \`create-extension\`. "Add an Unread tab to the left navigation", "make the subject lines wrap", "change the inbox grouping logic", "add a field to the compose form" → \`connect-builder\`.`
3083
3129
  : `### Extensions Disabled
3084
3130
 
3085
3131
  Extension creation and management tools are disabled for this app. Do not claim you can create, edit, hide, or delete Agent-Native extensions unless the template exposes its own typed action for that workflow. For requests that would otherwise be handled as an extension/widget/dashboard/calculator mini-app, explain that this app has disabled extension tools and use the app's available actions instead.`;
@@ -3088,6 +3134,8 @@ Extension creation and management tools are disabled for this app. Do not claim
3088
3134
 
3089
3135
  In Act mode, if the user asks for generated interactive UI in chat, call \`render-inline-extension\` for one-time inline controls/knobs/calculators/visualizers that do not need saving. If the user asks for an **extension**, **widget**, **dashboard**, **calculator**, or **mini-app** that should be reusable or saved, call \`create-extension\` with a self-contained Alpine.js HTML body. To load a saved extension inline, call \`show-extension-inline\`. These are NOT code changes — extensions are sandboxed mini-apps. Do not preface with "let me build…" — just call the right extension action.
3090
3136
 
3137
+ Use app-native artifact actions first when they exist for dashboards, reports, analyses, charts, documents, decks, or similar domain artifacts. Pick \`create-extension\` only for explicit extension/custom mini-app requests or for behavior the native artifact format cannot support.
3138
+
3091
3139
  Keep the first \`create-extension\` call compact and working. If the request is complex, create the v1 first and then refine with focused \`update-extension\` edits.
3092
3140
 
3093
3141
  Generated UI can read chat inputs from slotContext/window.onSlotContext, see/update app state through appFetch/appAction, use extensionData, record passive current values through agentNative.ui.output(value, opts?), and send visible results through agentNative.chat.send(...) or sendToAgentChat(...). ui.output writes \`inline-ui:<extensionId>:output\` in application state; when the user asks to use the current slider/selection/value, read \`readAppState("inline-ui:<id>:output")\`. Transient extensionData is browser-local only, so do not rely on it for values the agent or app must observe. Use semantic Tailwind theme classes.
@@ -4994,45 +5042,13 @@ export function createAgentChatPlugin(
4994
5042
  });
4995
5043
  }
4996
5044
 
4997
- type OwnerContext = {
4998
- owner: string;
4999
- anonymous: boolean;
5000
- name?: string;
5001
- };
5002
- const OWNER_CONTEXT_KEY = "__agentNativeOwnerContext";
5003
-
5004
5045
  // Resolve owner from the H3 event's session, with an optional
5005
5046
  // template-provided anonymous owner for public read-only surfaces.
5006
- const resolveOwnerContext = async (event: any): Promise<OwnerContext> => {
5007
- const eventContext = event?.context as
5008
- | (Record<string, unknown> & { [OWNER_CONTEXT_KEY]?: OwnerContext })
5009
- | undefined;
5010
- if (eventContext?.[OWNER_CONTEXT_KEY]) {
5011
- return eventContext[OWNER_CONTEXT_KEY];
5012
- }
5013
-
5014
- const session = await getSession(event);
5015
- if (session?.email) {
5016
- const resolved = {
5017
- owner: session.email,
5018
- anonymous: false,
5019
- name: session.name,
5020
- };
5021
- if (eventContext) eventContext[OWNER_CONTEXT_KEY] = resolved;
5022
- return resolved;
5023
- }
5024
-
5025
- const anonymousOwner = await options?.anonymousOwner?.(event);
5026
- if (anonymousOwner) {
5027
- const resolved = { owner: anonymousOwner, anonymous: true };
5028
- if (eventContext) eventContext[OWNER_CONTEXT_KEY] = resolved;
5029
- return resolved;
5030
- }
5031
-
5032
- const { createError } = await import("h3");
5033
- throw createError({
5034
- statusCode: 401,
5035
- statusMessage: "Unauthenticated",
5047
+ const resolveOwnerContext = async (
5048
+ event: any,
5049
+ ): Promise<AgentRunOwnerContext> => {
5050
+ return resolveAgentRunOwnerContext(event, {
5051
+ anonymousOwner: options?.anonymousOwner,
5036
5052
  });
5037
5053
  };
5038
5054
 
@@ -7749,92 +7765,17 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
7749
7765
  // the background worker), so both go through identical context + handler
7750
7766
  // selection.
7751
7767
  const invokeAgentChatHandler = async (event: any) => {
7752
- // Resolve per-request auth context
7768
+ // Resolve per-request auth context.
7753
7769
  const ownerContext = await resolveOwnerContext(event);
7754
- const owner = ownerContext.owner;
7755
-
7756
- // Resolve org ID: explicit callback > session.orgId from Better Auth
7757
- // > implicit org membership. Better Auth leaves session.orgId null
7758
- // until the user explicitly switches orgs, so a fresh signup with
7759
- // implicit membership (e.g. domain-matched org) would otherwise see
7760
- // no org-scoped credentials. getOrgContext() does the same DB lookup
7761
- // the /builder/status endpoint uses to decide "Connected".
7762
- let resolvedOrgId: string | undefined;
7763
- if (options?.resolveOrgId) {
7764
- resolvedOrgId = (await options.resolveOrgId(event)) ?? undefined;
7765
- } else {
7766
- try {
7767
- const session = await getSession(event);
7768
- resolvedOrgId = session?.orgId ?? undefined;
7769
- } catch {
7770
- // Session not available
7771
- }
7772
- if (!resolvedOrgId) {
7773
- try {
7774
- const { getOrgContext } = await import("../org/context.js");
7775
- const ctx = await getOrgContext(event);
7776
- resolvedOrgId = ctx.orgId ?? undefined;
7777
- } catch {
7778
- // org_members table may not exist yet on first boot
7779
- }
7780
- }
7781
- }
7782
- // Cookieless durable background worker: getSession()/getOrgContext()
7783
- // resolve from the (absent) session, so org-scoped credential resolution
7784
- // (the Builder engine) would miss the owner's org and resolveEngine would
7785
- // fall back to the anthropic default — bailing on a missing key before
7786
- // the worker claims its run. The owner IS known here (seeded via
7787
- // OWNER_CONTEXT_KEY from the run's thread), so resolve the org directly
7788
- // from the owner email, the same active-org logic the foreground session
7789
- // would yield.
7790
- if (!resolvedOrgId && owner) {
7791
- try {
7792
- const { resolveOrgIdForEmail } = await import("../org/context.js");
7793
- resolvedOrgId = (await resolveOrgIdForEmail(owner)) ?? undefined;
7794
- } catch {
7795
- // org tables unavailable — proceed without org (foreground-equivalent)
7796
- }
7797
- }
7798
-
7799
- // DURABLE OWNER CONTEXT, PART 2: ORG.
7800
- // The cookieless `_process-run` self-dispatch (and any other background
7801
- // re-entry) resolves the owner from the run row, but has NO session —
7802
- // every org resolver above ultimately derives identity from
7803
- // getSession(event), so they all come back null here. That silently
7804
- // scopes the agent to `org_id IS NULL` rows only: it sees and writes
7805
- // resources outside the user's org, while the foreground UI — which
7806
- // carries the session cookie — resolves the real org. The result is the
7807
- // observed split where the agent can't list/read the same resources the
7808
- // UI shows and writes new rows with a null org. Resolve the org from the
7809
- // now-known owner email (the same active-org / membership logic
7810
- // getOrgContext uses, minus the session) so the agent and the UI operate
7811
- // over the same org-scoped data.
7812
- if (!resolvedOrgId && owner && !ownerContext.anonymous) {
7813
- try {
7814
- const { resolveOrgIdForEmail } = await import("../org/context.js");
7815
- resolvedOrgId = (await resolveOrgIdForEmail(owner)) ?? undefined;
7816
- } catch {
7817
- // org tables unavailable (first boot) — leave org unscoped
7818
- }
7819
- }
7820
7770
 
7821
- // Propagate the caller's IANA timezone from `x-user-timezone` so that
7822
- // tool calls made by the agent (e.g. log-meal with no explicit date)
7823
- // resolve "today" in the user's local timezone instead of server UTC.
7824
- const tzRaw = getHeader(event, "x-user-timezone");
7825
- const timezone =
7826
- typeof tzRaw === "string" &&
7827
- tzRaw.trim().length > 0 &&
7828
- tzRaw.trim().length < 64
7829
- ? tzRaw.trim()
7830
- : undefined;
7831
-
7832
- return runWithRequestContext(
7771
+ return runWithAgentRunContext(
7833
7772
  {
7834
- userEmail: owner,
7835
- userName: ownerContext.name,
7836
- orgId: resolvedOrgId,
7837
- timezone,
7773
+ event,
7774
+ ownerContext,
7775
+ resolveOrgId: options?.resolveOrgId,
7776
+ isBackgroundWorker: Boolean(
7777
+ (event as any).context?.__agentChatBackgroundBody,
7778
+ ),
7838
7779
  },
7839
7780
  () => {
7840
7781
  // App-rendered chat can't host direct code edits — HMR/full
@@ -7949,50 +7890,29 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
7949
7890
  (event as any).context = (event as any).context ?? {};
7950
7891
  (event as any).context.__agentChatBackgroundBody = prepared.body;
7951
7892
 
7952
- // DURABLE OWNER CONTEXT: this self-dispatch is cookieless (HMAC-only
7953
- // the foreground POST carried the user's session, but this background-
7954
- // function invocation does not). Without a session, resolveOwnerContext()
7955
- // falls through to its 401 "Unauthenticated" throw and the worker dies
7956
- // before it can even claim the run (observed in prod as the `route_threw`
7957
- // diag stage — every durable run only completed via the foreground
7958
- // circuit-breaker's inline recovery). The run's chat thread already
7959
- // records the authenticated owner (written by the foreground under the
7960
- // signed-in user), so resolve the owner from the run row — NOT from the
7961
- // request body, which the HMAC does not cover and a caller could forge —
7962
- // and pre-seed the owner context the handler reads (resolveOwnerContext
7963
- // returns it before attempting any session lookup).
7964
- let backgroundOwner: string | null = null;
7965
- try {
7966
- const { getRunOwnerEmail } = await import("../agent/run-store.js");
7967
- backgroundOwner = await getRunOwnerEmail(prepared.runId);
7968
- if (backgroundOwner) {
7969
- (event as any).context[OWNER_CONTEXT_KEY] = {
7970
- owner: backgroundOwner,
7971
- anonymous: false,
7972
- };
7973
- }
7974
- } catch {
7975
- // Best-effort: if the lookup fails, resolveOwnerContext runs as before
7976
- // and throws its normal 401, surfaced via the route_threw diagnostic.
7977
- }
7893
+ // Durable owner context: this self-dispatch is cookieless (HMAC-only).
7894
+ // Resolve the owner from the persisted run row, never the request body,
7895
+ // then invoke the normal handler. The shared agent-run context helper
7896
+ // expands that owner into the same user/org AsyncLocalStorage context the
7897
+ // foreground request uses, so credential and data scoping stay aligned.
7898
+ await seedBackgroundAgentRunOwnerContext(event, prepared.runId);
7978
7899
 
7979
7900
  try {
7980
- // Run the worker INSIDE the owner's AsyncLocalStorage request context.
7981
- // Seeding OWNER_CONTEXT_KEY (above) only feeds getOwnerFromEvent; the
7982
- // engine-resolution path (detectEngineFromUserSecrets) and other
7983
- // owner-scoped reads use getRequestUserEmail()/getRequestOrgId(),
7984
- // which a cookieless background worker otherwise leaves empty. Without
7985
- // this, resolveEngine can't see the owner's Builder credential, falls
7986
- // back to the anthropic default, and — when the owner has no stored
7987
- // anthropic key and deploy-credential fallback is blocked — bails at
7988
- // the API-key check BEFORE claiming its run (the analytics durable
7989
- // worker's "stalls at post_model, never claims" bug).
7990
- const invoke = () => invokeAgentChatHandler(event);
7991
- return await (backgroundOwner
7992
- ? runWithRequestContext({ userEmail: backgroundOwner }, invoke)
7993
- : invoke());
7901
+ return await invokeAgentChatHandler(event);
7994
7902
  } catch (err: any) {
7995
7903
  console.error("[agent-chat] _process-run failed:", err);
7904
+ captureError(err, {
7905
+ route: AGENT_CHAT_PROCESS_RUN_PATH,
7906
+ method: getMethod(event),
7907
+ userAgent: getHeader(event, "user-agent"),
7908
+ tags: {
7909
+ source: "agent-chat-bg-worker",
7910
+ phase: "process-run",
7911
+ },
7912
+ extra: {
7913
+ runId: prepared.runId,
7914
+ },
7915
+ });
7996
7916
  // DIAGNOSTIC: the worker invocation threw at the route boundary —
7997
7917
  // record the message so the failure cause is readable client-side.
7998
7918
  if (diag) {
@@ -0,0 +1,204 @@
1
+ import { createError, getHeader, type H3Event } from "h3";
2
+
3
+ import { resolveOrgIdForEmail, getOrgContext } from "../org/context.js";
4
+ import { getSession } from "./auth.js";
5
+ import {
6
+ runWithRequestContext,
7
+ type RequestContext,
8
+ } from "./request-context.js";
9
+
10
+ export type AgentRunOwnerContext = {
11
+ owner: string;
12
+ anonymous: boolean;
13
+ name?: string;
14
+ };
15
+
16
+ export const AGENT_RUN_OWNER_CONTEXT_KEY = "__agentNativeOwnerContext";
17
+
18
+ type EventWithAgentRunContext = H3Event & {
19
+ context?: Record<string, unknown>;
20
+ };
21
+
22
+ type AnonymousOwnerResolver = (
23
+ event: H3Event,
24
+ ) => string | null | Promise<string | null>;
25
+
26
+ type OrgIdResolver = (
27
+ event: H3Event,
28
+ ) => string | null | undefined | Promise<string | null | undefined>;
29
+
30
+ function eventContext(
31
+ event: EventWithAgentRunContext,
32
+ ): Record<string, unknown> {
33
+ event.context = event.context ?? {};
34
+ return event.context;
35
+ }
36
+
37
+ function normalizeId(value: string | null | undefined): string | undefined {
38
+ return typeof value === "string" && value.trim().length > 0
39
+ ? value.trim()
40
+ : undefined;
41
+ }
42
+
43
+ function readHeaderValue(event: any, name: string): unknown {
44
+ try {
45
+ const value = getHeader(event, name);
46
+ if (value !== undefined && value !== null) return value;
47
+ } catch {
48
+ // Unit tests and a few framework internals pass lightweight event shims.
49
+ }
50
+
51
+ const headers = event?.headers;
52
+ if (headers && typeof headers.get === "function") {
53
+ return headers.get(name) ?? headers.get(name.toLowerCase()) ?? undefined;
54
+ }
55
+
56
+ const reqHeaders = event?.node?.req?.headers ?? event?.req?.headers;
57
+ if (reqHeaders && typeof reqHeaders === "object") {
58
+ return reqHeaders[name] ?? reqHeaders[name.toLowerCase()];
59
+ }
60
+
61
+ return undefined;
62
+ }
63
+
64
+ export function readAgentRunTimezone(event: H3Event): string | undefined {
65
+ const raw = readHeaderValue(event, "x-user-timezone");
66
+ const value = Array.isArray(raw) ? raw[0] : raw;
67
+ return typeof value === "string" &&
68
+ value.trim().length > 0 &&
69
+ value.trim().length < 64
70
+ ? value.trim()
71
+ : undefined;
72
+ }
73
+
74
+ export function seedAgentRunOwnerContext(
75
+ event: H3Event,
76
+ ownerContext: AgentRunOwnerContext,
77
+ ): AgentRunOwnerContext {
78
+ eventContext(event as EventWithAgentRunContext)[AGENT_RUN_OWNER_CONTEXT_KEY] =
79
+ ownerContext;
80
+ return ownerContext;
81
+ }
82
+
83
+ export async function seedBackgroundAgentRunOwnerContext(
84
+ event: H3Event,
85
+ runId: string,
86
+ ): Promise<AgentRunOwnerContext | null> {
87
+ try {
88
+ const { getRunOwnerEmail } = await import("../agent/run-store.js");
89
+ const owner = await getRunOwnerEmail(runId);
90
+ if (!owner) return null;
91
+ return seedAgentRunOwnerContext(event, { owner, anonymous: false });
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ export async function resolveAgentRunOwnerContext(
98
+ event: H3Event,
99
+ options: { anonymousOwner?: AnonymousOwnerResolver } = {},
100
+ ): Promise<AgentRunOwnerContext> {
101
+ const ctx = eventContext(event as EventWithAgentRunContext);
102
+ const seeded = ctx[AGENT_RUN_OWNER_CONTEXT_KEY] as
103
+ | AgentRunOwnerContext
104
+ | undefined;
105
+ if (seeded) return seeded;
106
+
107
+ const session = await getSession(event);
108
+ if (session?.email) {
109
+ return seedAgentRunOwnerContext(event, {
110
+ owner: session.email,
111
+ anonymous: false,
112
+ name: session.name,
113
+ });
114
+ }
115
+
116
+ const anonymousOwner = await options.anonymousOwner?.(event);
117
+ if (anonymousOwner) {
118
+ return seedAgentRunOwnerContext(event, {
119
+ owner: anonymousOwner,
120
+ anonymous: true,
121
+ });
122
+ }
123
+
124
+ throw createError({
125
+ statusCode: 401,
126
+ statusMessage: "Unauthenticated",
127
+ });
128
+ }
129
+
130
+ export async function resolveAgentRunOrgId(options: {
131
+ event: H3Event;
132
+ ownerContext: AgentRunOwnerContext;
133
+ resolveOrgId?: OrgIdResolver;
134
+ }): Promise<string | undefined> {
135
+ let resolvedOrgId: string | undefined;
136
+
137
+ if (options.resolveOrgId) {
138
+ resolvedOrgId = normalizeId(await options.resolveOrgId(options.event));
139
+ } else {
140
+ try {
141
+ const session = await getSession(options.event);
142
+ resolvedOrgId = normalizeId(session?.orgId);
143
+ } catch {
144
+ // Session not available.
145
+ }
146
+
147
+ if (!resolvedOrgId) {
148
+ try {
149
+ const orgContext = await getOrgContext(options.event);
150
+ resolvedOrgId = normalizeId(orgContext.orgId);
151
+ } catch {
152
+ // Org tables may not exist yet on first boot.
153
+ }
154
+ }
155
+ }
156
+
157
+ if (
158
+ !resolvedOrgId &&
159
+ options.ownerContext.owner &&
160
+ !options.ownerContext.anonymous
161
+ ) {
162
+ try {
163
+ resolvedOrgId = normalizeId(
164
+ await resolveOrgIdForEmail(options.ownerContext.owner),
165
+ );
166
+ } catch {
167
+ // Org tables may not exist yet on first boot.
168
+ }
169
+ }
170
+
171
+ return resolvedOrgId;
172
+ }
173
+
174
+ export async function resolveAgentRunRequestContext(options: {
175
+ event: H3Event;
176
+ ownerContext: AgentRunOwnerContext;
177
+ resolveOrgId?: OrgIdResolver;
178
+ isBackgroundWorker?: boolean;
179
+ }): Promise<RequestContext> {
180
+ const orgId = await resolveAgentRunOrgId(options);
181
+ const timezone = readAgentRunTimezone(options.event);
182
+ return {
183
+ userEmail: options.ownerContext.owner,
184
+ userName: options.ownerContext.name,
185
+ orgId,
186
+ timezone,
187
+ ...(options.isBackgroundWorker
188
+ ? { run: { isBackgroundWorker: true } }
189
+ : {}),
190
+ };
191
+ }
192
+
193
+ export async function runWithAgentRunContext<T>(
194
+ options: {
195
+ event: H3Event;
196
+ ownerContext: AgentRunOwnerContext;
197
+ resolveOrgId?: OrgIdResolver;
198
+ isBackgroundWorker?: boolean;
199
+ },
200
+ fn: () => T | Promise<T>,
201
+ ): Promise<T> {
202
+ const requestContext = await resolveAgentRunRequestContext(options);
203
+ return await runWithRequestContext(requestContext, fn);
204
+ }
@@ -80,7 +80,7 @@ export function readDeployCredentialEnv(key: string): string | undefined {
80
80
  * key does not silently power another tenant's chat.
81
81
  */
82
82
  export function isDeployCredentialFallbackAllowed(): boolean {
83
- if (process.env.NODE_ENV !== "production") return true;
83
+ if (!isProductionLikeRuntime()) return true;
84
84
  return isLocalDatabase();
85
85
  }
86
86
 
@@ -121,6 +121,22 @@ function isHostedWorkspaceRuntime(): boolean {
121
121
  );
122
122
  }
123
123
 
124
+ function isProductionLikeRuntime(): boolean {
125
+ return (
126
+ process.env.NODE_ENV === "production" ||
127
+ /^(1|true)$/i.test(process.env.NETLIFY ?? "") ||
128
+ /^(1|true)$/i.test(process.env.VERCEL ?? "") ||
129
+ /^(1|true)$/i.test(process.env.CF_PAGES ?? "") ||
130
+ Boolean(
131
+ process.env.AWS_LAMBDA_FUNCTION_NAME ||
132
+ process.env.AWS_EXECUTION_ENV ||
133
+ process.env.FUNCTIONS_WORKER_RUNTIME ||
134
+ process.env.K_SERVICE ||
135
+ process.env.RENDER,
136
+ )
137
+ );
138
+ }
139
+
124
140
  /**
125
141
  * Whether deployment-level Builder env keys may back the current request.
126
142
  *
@@ -143,18 +159,18 @@ function canUseBuilderDeployCredentialFallbackForRequest(): boolean {
143
159
  const email = getRequestUserEmail();
144
160
  if (!email) return true;
145
161
 
146
- const isProduction = process.env.NODE_ENV === "production";
162
+ const isProductionRuntime = isProductionLikeRuntime();
147
163
  // Local dogfooding escape hatch: lets the env / root-`.env` Builder key back
148
164
  // a signed-in user so running the app locally doesn't require completing the
149
165
  // Builder connect flow first. Non-prod only.
150
166
  const localDevOptIn =
151
- !isProduction &&
167
+ !isProductionRuntime &&
152
168
  /^(1|true)$/i.test(process.env.AGENT_NATIVE_LOCAL_BUILDER_ENV ?? "");
153
169
 
154
170
  if (isHostedWorkspaceRuntime() && !localDevOptIn) return false;
155
171
 
156
172
  // Deploy fallback is safe in non-prod or on a local/single-tenant database.
157
- return !isProduction || isLocalDatabase();
173
+ return !isProductionRuntime || isLocalDatabase();
158
174
  }
159
175
 
160
176
  function shouldTraceCredentialResolve(): boolean {
@@ -593,8 +593,18 @@ function ChartTooltip({
593
593
  left: number;
594
594
  top: number;
595
595
  } | null>(null);
596
- const items = sortTooltipPayloadItems(
597
- payload?.filter((item) => item.value != null && item.value !== "") ?? [],
596
+ const items = useMemo(
597
+ () =>
598
+ sortTooltipPayloadItems(
599
+ payload?.filter((item) => item.value != null && item.value !== "") ??
600
+ [],
601
+ ),
602
+ [payload],
603
+ );
604
+ const portalVisible = portalPosition !== null;
605
+ const clearPortalPosition = useCallback(
606
+ () => setPortalPosition((prev) => (prev === null ? prev : null)),
607
+ [],
598
608
  );
599
609
 
600
610
  const labelText =
@@ -606,7 +616,7 @@ function ChartTooltip({
606
616
 
607
617
  useBrowserLayoutEffect(() => {
608
618
  if (!active || items.length === 0 || typeof window === "undefined") {
609
- setPortalPosition(null);
619
+ clearPortalPosition();
610
620
  return;
611
621
  }
612
622
 
@@ -688,7 +698,7 @@ function ChartTooltip({
688
698
  window.removeEventListener("resize", schedule);
689
699
  window.removeEventListener("scroll", schedule, true);
690
700
  };
691
- });
701
+ }, [active, clearPortalPosition, items.length, portalVisible]);
692
702
 
693
703
  if (!active || items.length === 0) return null;
694
704
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-26
4
+ ---
5
+
6
+ Session replay uploads now accept compressed payloads so larger recordings ingest more reliably.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-26
4
+ ---
5
+
6
+ SQL chart tooltips now stay stable while moving across dense charts.