@agent-native/core 0.84.14 → 0.84.16

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 (59) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +17 -0
  3. package/corpus/core/package.json +2 -2
  4. package/corpus/core/src/agent/durable-background.ts +6 -0
  5. package/corpus/core/src/agent/engine/builder-engine.ts +7 -3
  6. package/corpus/core/src/agent/production-agent.ts +10 -3
  7. package/corpus/core/src/client/AssistantChat.tsx +30 -4
  8. package/corpus/core/src/client/MultiTabAssistantChat.tsx +11 -3
  9. package/corpus/core/src/client/agent-chat.ts +18 -3
  10. package/corpus/core/src/client/use-agent-engine-configured.ts +7 -4
  11. package/corpus/core/src/db/client.ts +10 -1
  12. package/corpus/core/src/server/agent-chat-plugin.ts +69 -43
  13. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +18 -24
  14. package/corpus/templates/analytics/changelog/2026-07-01-dashboard-charts-no-longer-show-future-dates-when-clients-se.md +6 -0
  15. package/corpus/templates/analytics/changelog/2026-07-01-dashboard-filters-take-less-space-and-keep-view-actions-tucked.md +6 -0
  16. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +34 -9
  17. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +6 -2
  18. package/corpus/templates/analytics/server/lib/session-replay.ts +109 -50
  19. package/corpus/templates/analytics/server/plugins/db.ts +21 -0
  20. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  21. package/corpus/templates/design/app/pages/DesignEditor.tsx +176 -13
  22. package/corpus/templates/design/changelog/2026-07-01-exporting-a-design-as-png-or-svg-now-downloads-just-the-sele.md +6 -0
  23. package/dist/agent/durable-background.d.ts +1 -0
  24. package/dist/agent/durable-background.d.ts.map +1 -1
  25. package/dist/agent/durable-background.js +3 -0
  26. package/dist/agent/durable-background.js.map +1 -1
  27. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  28. package/dist/agent/engine/builder-engine.js +7 -1
  29. package/dist/agent/engine/builder-engine.js.map +1 -1
  30. package/dist/agent/production-agent.d.ts.map +1 -1
  31. package/dist/agent/production-agent.js +6 -4
  32. package/dist/agent/production-agent.js.map +1 -1
  33. package/dist/client/AssistantChat.d.ts +8 -2
  34. package/dist/client/AssistantChat.d.ts.map +1 -1
  35. package/dist/client/AssistantChat.js +18 -2
  36. package/dist/client/AssistantChat.js.map +1 -1
  37. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  38. package/dist/client/MultiTabAssistantChat.js +7 -3
  39. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  40. package/dist/client/agent-chat.d.ts +7 -0
  41. package/dist/client/agent-chat.d.ts.map +1 -1
  42. package/dist/client/agent-chat.js +5 -1
  43. package/dist/client/agent-chat.js.map +1 -1
  44. package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
  45. package/dist/client/use-agent-engine-configured.js +7 -1
  46. package/dist/client/use-agent-engine-configured.js.map +1 -1
  47. package/dist/collab/awareness.d.ts +2 -2
  48. package/dist/collab/awareness.d.ts.map +1 -1
  49. package/dist/db/client.d.ts +6 -0
  50. package/dist/db/client.d.ts.map +1 -1
  51. package/dist/db/client.js +8 -1
  52. package/dist/db/client.js.map +1 -1
  53. package/dist/notifications/routes.d.ts +3 -3
  54. package/dist/observability/routes.d.ts +6 -6
  55. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  56. package/dist/server/agent-chat-plugin.js +57 -40
  57. package/dist/server/agent-chat-plugin.js.map +1 -1
  58. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  59. package/package.json +2 -2
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2043
31
- - template files: 4919
31
+ - template files: 4922
@@ -1,5 +1,22 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.84.16
4
+
5
+ ### Patch Changes
6
+
7
+ - 564460a: Add an optional `focus` flag to `setAgentChatContextItem` (and thread it through to the composer). Callers that mirror ambient UI state into chat context — such as a design canvas element selection — can now pass `focus: false` to stage the context chip without moving keyboard focus into the composer. This stops passive context staging from blurring and tearing down an in-progress inline text editor in the Design canvas (which re-fires on every selection and on each get-design poll during an agent run). Focus stays enabled by default, so existing callers are unchanged.
8
+ - 2b27c0f: Avoid unsupported array helpers in Builder engine message caching and stabilize prep-load tests.
9
+ - a4f5303: Keep background chat continuation markers and database pool detection aligned with the actual background function dispatch path.
10
+
11
+ ## 0.84.15
12
+
13
+ ### Patch Changes
14
+
15
+ - 946ff86: Fix the "No LLM provider is connected" chat banner staying visible after a
16
+ provider is connected. The composer now clears that error once the engine
17
+ status flips to configured, and the status check no longer reads a stale
18
+ cached "missing" response after connecting.
19
+
3
20
  ## 0.84.14
4
21
 
5
22
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.14",
3
+ "version": "0.84.16",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -184,7 +184,7 @@
184
184
  "build": "tsgo && tsgo -p tsconfig.cli.json && node scripts/finalize-build.mjs && node scripts/check-dist-imports.mjs",
185
185
  "dev": "tsgo --watch",
186
186
  "typecheck": "tsgo --noEmit",
187
- "test": "vitest --run src",
187
+ "test": "vitest --run src --maxWorkers=25%",
188
188
  "prepack": "tsx ../../scripts/sync-workspace-core-skills.ts --check && npm run build && cp ../../README.md ./README.md",
189
189
  "prepublishOnly": "npm run build"
190
190
  },
@@ -149,6 +149,12 @@ export function resolveAgentChatProcessRunDispatchPath(): string {
149
149
  return AGENT_CHAT_PROCESS_RUN_PATH;
150
150
  }
151
151
 
152
+ export function dispatchPathTargetsNetlifyBackgroundFunction(
153
+ dispatchPath: string,
154
+ ): boolean {
155
+ return dispatchPath.startsWith("/.netlify/functions/");
156
+ }
157
+
152
158
  /**
153
159
  * Env flag for durable background runs. DEFAULT-OFF (opt-in): unset means
154
160
  * disabled; an app opts IN with an explicit truthy value (`true`/`1`/`yes`/`on`).
@@ -193,9 +193,13 @@ class BuilderEngine implements AgentEngine {
193
193
  // content block so the entire conversation prefix is cached.
194
194
  let cachedMessages = messages;
195
195
  if (cacheEnabled && messages.length > 0) {
196
- const lastUserIdx = [...messages].findLastIndex(
197
- (m: any) => m.role === "user",
198
- );
196
+ let lastUserIdx = -1;
197
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
198
+ if ((messages[i] as any).role === "user") {
199
+ lastUserIdx = i;
200
+ break;
201
+ }
202
+ }
199
203
  if (lastUserIdx >= 0) {
200
204
  cachedMessages = [...messages];
201
205
  const lastMsg = { ...cachedMessages[lastUserIdx] } as any;
@@ -54,6 +54,7 @@ import { computeProtectedSegmentIds } from "./context-xray/segments.js";
54
54
  import {
55
55
  AGENT_CHAT_BACKGROUND_RUN_FIELD,
56
56
  backgroundRuntimeDiagnosticDetail,
57
+ dispatchPathTargetsNetlifyBackgroundFunction,
57
58
  isAgentChatDurableBackgroundEnabled,
58
59
  resolveAgentChatProcessRunDispatchPath,
59
60
  shouldUseBackgroundFunctionTimeoutForWorker,
@@ -5017,7 +5018,7 @@ export function createProductionAgentHandler(
5017
5018
  let dispatched = false;
5018
5019
  const backgroundDispatchPath = resolveAgentChatProcessRunDispatchPath();
5019
5020
  const expectsNetlifyBackgroundFunction =
5020
- backgroundDispatchPath.startsWith("/.netlify/functions/");
5021
+ dispatchPathTargetsNetlifyBackgroundFunction(backgroundDispatchPath);
5021
5022
  try {
5022
5023
  await fireInternalDispatch({
5023
5024
  event,
@@ -5313,6 +5314,12 @@ export function createProductionAgentHandler(
5313
5314
  // its seq log starts clean; same turnId folds the assistant
5314
5315
  // message across chunks.
5315
5316
  const nextRunId = generateRunId();
5317
+ const continuationDispatchPath =
5318
+ resolveAgentChatProcessRunDispatchPath();
5319
+ const continuationExpectsNetlifyBackgroundFunction =
5320
+ dispatchPathTargetsNetlifyBackgroundFunction(
5321
+ continuationDispatchPath,
5322
+ );
5316
5323
  try {
5317
5324
  await fireInternalDispatch({
5318
5325
  event,
@@ -5322,7 +5329,7 @@ export function createProductionAgentHandler(
5322
5329
  // background:true; never shadowed because /.netlify/* is
5323
5330
  // excluded from the /* catch-all) so each chunk keeps the
5324
5331
  // 15-min budget; off-Netlify the in-process framework route.
5325
- path: resolveAgentChatProcessRunDispatchPath(),
5332
+ path: continuationDispatchPath,
5326
5333
  taskId: nextRunId,
5327
5334
  body: {
5328
5335
  ...body,
@@ -5332,7 +5339,7 @@ export function createProductionAgentHandler(
5332
5339
  turnId: effectiveTurnId,
5333
5340
  continuationCount: backgroundContinuationCount + 1,
5334
5341
  backgroundFunctionRuntimeExpected:
5335
- runsInBackgroundFunction,
5342
+ continuationExpectsNetlifyBackgroundFunction,
5336
5343
  },
5337
5344
  },
5338
5345
  });
@@ -648,8 +648,15 @@ export interface AssistantChatHandle {
648
648
  ): void;
649
649
  /** Programmatically prefill the composer without submitting. */
650
650
  prefillMessage(text: string): void;
651
- /** Add or replace keyed context for the next composer submission. */
652
- setComposerContextItem(item: AgentChatContextItem): void;
651
+ /**
652
+ * Add or replace keyed context for the next composer submission.
653
+ * Focuses the composer by default; pass `{ focus: false }` for passive
654
+ * context mirroring (e.g. canvas selection) that must not steal focus.
655
+ */
656
+ setComposerContextItem(
657
+ item: AgentChatContextItem,
658
+ options?: { focus?: boolean },
659
+ ): void;
653
660
  /** Remove a keyed context item from the composer. */
654
661
  removeComposerContextItem(key: string): void;
655
662
  /** Clear all staged context items from the composer. */
@@ -1205,6 +1212,17 @@ const AssistantChatInner = forwardRef<
1205
1212
  );
1206
1213
  const missingApiKey = agentEngineConfigured.missing;
1207
1214
  const isComposerDisabled = missingApiKey || composerDisabled;
1215
+ // Once a provider connects, `useAgentEngineConfigured` flips to "configured"
1216
+ // off the `agent-engine:configured-changed` event. The composer banner is a
1217
+ // separate piece of state that would otherwise only clear on manual dismiss
1218
+ // or the next successful send, so drop the stale "no LLM provider" message
1219
+ // here to match the resolved connection.
1220
+ useEffect(() => {
1221
+ if (agentEngineConfigured.state !== "configured") return;
1222
+ setComposerError((current) =>
1223
+ current === LLM_MISSING_CREDENTIALS_MESSAGE ? null : current,
1224
+ );
1225
+ }, [agentEngineConfigured.state]);
1208
1226
  const missingApiKeySetupAboveComposer =
1209
1227
  missingApiKeySetupLayout === "sidebar";
1210
1228
  // Increments each time the user tries to chat while no LLM is connected.
@@ -2958,9 +2976,17 @@ const AssistantChatInner = forwardRef<
2958
2976
  tiptapRef.current?.setText(text);
2959
2977
  tiptapRef.current?.focus();
2960
2978
  },
2961
- setComposerContextItem(item: AgentChatContextItem) {
2979
+ setComposerContextItem(
2980
+ item: AgentChatContextItem,
2981
+ options?: { focus?: boolean },
2982
+ ) {
2962
2983
  stageComposerContextItem(item);
2963
- tiptapRef.current?.focus();
2984
+ // Only pull focus into the composer for explicit user gestures.
2985
+ // Passive context mirroring (e.g. a canvas selection staged with
2986
+ // openSidebar:false) must not steal focus — doing so blurs an active
2987
+ // inline text editor living in the design canvas iframe and tears it
2988
+ // down the instant it opens.
2989
+ if (options?.focus !== false) tiptapRef.current?.focus();
2964
2990
  },
2965
2991
  removeComposerContextItem(key: string) {
2966
2992
  removeComposerContextItem(key);
@@ -1118,10 +1118,14 @@ export function MultiTabAssistantChat({
1118
1118
  const postMessageSubmissionsDisabled = props.composerDisabled === true;
1119
1119
 
1120
1120
  const setContextInTab = useCallback(
1121
- (threadId: string, item: AgentChatContextItem) => {
1121
+ (
1122
+ threadId: string,
1123
+ item: AgentChatContextItem,
1124
+ options?: { focus?: boolean },
1125
+ ) => {
1122
1126
  const ref = chatRefs.current.get(threadId);
1123
1127
  if (ref) {
1124
- ref.setComposerContextItem(item);
1128
+ ref.setComposerContextItem(item, options);
1125
1129
  return;
1126
1130
  }
1127
1131
  const existing = pendingContextItems.current.get(threadId) ?? [];
@@ -1712,7 +1716,11 @@ export function MultiTabAssistantChat({
1712
1716
  if (postMessageSubmissionsDisabled) return;
1713
1717
  const currentTabId = activeThreadIdRef.current;
1714
1718
  if (!currentTabId) return;
1715
- setContextInTab(currentTabId, item);
1719
+ // Focus defaults to true; a caller opts out with `focus: false` for
1720
+ // passive context (e.g. a canvas selection) so staging never steals
1721
+ // focus from an in-progress inline editor.
1722
+ const focus = (event.data.data?.focus as boolean | undefined) !== false;
1723
+ setContextInTab(currentTabId, item, { focus });
1716
1724
  return;
1717
1725
  }
1718
1726
  if (event.data?.type === AGENT_CHAT_REMOVE_CONTEXT_MESSAGE_TYPE) {
@@ -113,6 +113,13 @@ export interface AgentChatContextSetOptions extends AgentChatContextItem {
113
113
  * Defaults to true so the user can see the staged context.
114
114
  */
115
115
  openSidebar?: boolean;
116
+ /**
117
+ * Whether to move keyboard focus into the composer when staging the item.
118
+ * Defaults to true. Pass `false` for context that mirrors ambient UI state
119
+ * (e.g. a canvas element selection) so staging never steals focus from an
120
+ * unrelated editor — such as an inline text editor in a design canvas.
121
+ */
122
+ focus?: boolean;
116
123
  }
117
124
 
118
125
  /** @deprecated Use `AgentChatContextSetOptions` instead. */
@@ -865,9 +872,17 @@ export function setAgentChatContextItem(
865
872
  publishAgentChatContextItems(
866
873
  withReplacedAgentChatContextItem(agentChatContextState.items, item),
867
874
  );
868
- postAgentChatContextMessage(AGENT_CHAT_SET_CONTEXT_MESSAGE_TYPE, item, {
869
- openSidebar: opts.openSidebar !== false,
870
- });
875
+ // Forward an explicit `focus: false` so the receiving composer can stage the
876
+ // chip without stealing focus. Focus stays enabled by default (field omitted)
877
+ // for every existing caller.
878
+ const messageData = opts.focus === false ? { ...item, focus: false } : item;
879
+ postAgentChatContextMessage(
880
+ AGENT_CHAT_SET_CONTEXT_MESSAGE_TYPE,
881
+ messageData,
882
+ {
883
+ openSidebar: opts.openSidebar !== false,
884
+ },
885
+ );
871
886
  }
872
887
 
873
888
  /** @deprecated Use `setAgentChatContextItem` instead. */
@@ -35,10 +35,13 @@ async function fetchStatusJson(
35
35
  }, timeoutMs);
36
36
  });
37
37
 
38
- const request = fetch(
39
- agentNativePath(path),
40
- controller ? { signal: controller.signal } : undefined,
41
- )
38
+ // Never serve a stale status from the HTTP cache: this is re-fetched right
39
+ // after a provider connects, and a cached "missing" would keep the composer
40
+ // gate and error banner pinned even though a provider is now configured.
41
+ const request = fetch(agentNativePath(path), {
42
+ cache: "no-store",
43
+ ...(controller ? { signal: controller.signal } : {}),
44
+ })
42
45
  .then((r) => (r.ok ? r.json() : null))
43
46
  .catch(() => null)
44
47
  .finally(() => {
@@ -837,13 +837,22 @@ export function neonPoolMax(): number {
837
837
  * (agent/durable-background.ts), replicated here to avoid a `db` → `agent`
838
838
  * import cycle. Keep the signals in sync with that function.
839
839
  */
840
- function isBackgroundFunctionPoolContext(): boolean {
840
+ export function isBackgroundFunctionPoolContext(): boolean {
841
841
  if (
842
842
  (globalThis as Record<string, unknown>)
843
843
  .__AGENT_NATIVE_BACKGROUND_RUNTIME__ === true
844
844
  ) {
845
845
  return true;
846
846
  }
847
+ // Set by the HMAC-authenticated agent-chat `_process-run` route before it
848
+ // re-enters the normal chat handler. This mirrors the marker-only runtime
849
+ // proof used by durable-background.ts without importing agent code into db/.
850
+ if (
851
+ (globalThis as Record<string, unknown>)
852
+ .__AGENT_NATIVE_BACKGROUND_RUNTIME_EXPECTED__ === true
853
+ ) {
854
+ return true;
855
+ }
847
856
  const lambdaName = process.env.AWS_LAMBDA_FUNCTION_NAME;
848
857
  if (
849
858
  typeof lambdaName === "string" &&
@@ -34,8 +34,9 @@ import {
34
34
  } from "../agent/app-model-defaults.js";
35
35
  import { DEFAULT_ANTHROPIC_MODEL } from "../agent/default-model.js";
36
36
  import {
37
+ AGENT_CHAT_BACKGROUND_RUN_FIELD,
37
38
  AGENT_CHAT_PROCESS_RUN_PATH,
38
- extractProcessRunId,
39
+ backgroundRunMarkerExpectsBackgroundRuntime,
39
40
  isInBackgroundFunctionRuntime,
40
41
  prepareProcessRunRequest,
41
42
  } from "../agent/durable-background.js";
@@ -7993,18 +7994,6 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
7993
7994
  setResponseStatus(event, 405);
7994
7995
  return { error: "Method not allowed" };
7995
7996
  }
7996
- // DIAGNOSTIC: load the run-store diagnostic recorder. Each stage we
7997
- // reach is written onto the run row (diag_stage) so a silent failure
7998
- // INSIDE the Netlify background function — whose logs we cannot read —
7999
- // is still diagnosable from the client via /runs/active. Best-effort:
8000
- // the import + every record call is wrapped so diagnostics can never
8001
- // break the worker path.
8002
- const diag = await import("../agent/run-store.js")
8003
- .then((m) => ({
8004
- record: m.recordRunDiagnostic,
8005
- stages: m.RUN_DIAG_STAGE,
8006
- }))
8007
- .catch(() => null);
8008
7997
 
8009
7998
  // Consume the body ONCE (h3 v2's web Request stream is single-use).
8010
7999
  let processBody: any;
@@ -8015,18 +8004,6 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
8015
8004
  return { error: "Invalid request body" };
8016
8005
  }
8017
8006
 
8018
- // Record "the route handler was entered" against the run BEFORE auth
8019
- // runs. This is the proof the bg-fn invocation actually reached Nitro
8020
- // (vs. dying at the function entry / never being invoked). The runId
8021
- // is parsed without authenticating so we can attach it even on a
8022
- // subsequent auth failure.
8023
- const diagRunId = extractProcessRunId(processBody);
8024
- if (diag && diagRunId) {
8025
- await diag
8026
- .record(diagRunId, diag.stages.routeEntered)
8027
- .catch(() => {});
8028
- }
8029
-
8030
8007
  // Validate + HMAC-authenticate the self-dispatch and prepare the
8031
8008
  // background-worker body. Pure decision (unit-tested in
8032
8009
  // durable-background.spec.ts); the route only wires it to h3.
@@ -8041,6 +8018,12 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
8041
8018
  // bypassing session auth) inside the unreadable bg function would
8042
8019
  // leave the run to time out with NO clue. The detail carries the
8043
8020
  // status + whether A2A_SECRET is even present in this isolate.
8021
+ const diag = await import("../agent/run-store.js")
8022
+ .then((m) => ({
8023
+ record: m.recordRunDiagnostic,
8024
+ stages: m.RUN_DIAG_STAGE,
8025
+ }))
8026
+ .catch(() => null);
8044
8027
  if (diag && prepared.runId) {
8045
8028
  const a2aPresent = Boolean(
8046
8029
  process.env.A2A_SECRET && process.env.A2A_SECRET.length > 0,
@@ -8057,27 +8040,58 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
8057
8040
  return { error: prepared.error };
8058
8041
  }
8059
8042
 
8060
- // DIAGNOSTIC: auth + body validation passed. Reaching here proves the
8061
- // request was authenticated and we are about to invoke the worker.
8062
- if (diag) {
8063
- await diag
8064
- .record(prepared.runId, diag.stages.authPassed)
8065
- .catch(() => {});
8043
+ const preparedMarker = (prepared.body as Record<string, unknown>)[
8044
+ AGENT_CHAT_BACKGROUND_RUN_FIELD
8045
+ ];
8046
+ const expectsBackgroundRuntime =
8047
+ backgroundRunMarkerExpectsBackgroundRuntime(preparedMarker);
8048
+ const runtimeGlobals = globalThis as Record<string, unknown>;
8049
+ const hadExpectedRuntimeMarker = Object.prototype.hasOwnProperty.call(
8050
+ runtimeGlobals,
8051
+ "__AGENT_NATIVE_BACKGROUND_RUNTIME_EXPECTED__",
8052
+ );
8053
+ const previousExpectedRuntimeMarker =
8054
+ runtimeGlobals.__AGENT_NATIVE_BACKGROUND_RUNTIME_EXPECTED__;
8055
+ if (expectsBackgroundRuntime) {
8056
+ runtimeGlobals.__AGENT_NATIVE_BACKGROUND_RUNTIME_EXPECTED__ = true;
8066
8057
  }
8067
8058
 
8068
- // Stash the verified+augmented body for the handler — the body stream
8069
- // is already consumed, so the handler reads this instead.
8070
- (event as any).context = (event as any).context ?? {};
8071
- (event as any).context.__agentChatBackgroundBody = prepared.body;
8072
-
8073
- // Durable owner context: this self-dispatch is cookieless (HMAC-only).
8074
- // Resolve the owner from the persisted run row, never the request body,
8075
- // then invoke the normal handler. The shared agent-run context helper
8076
- // expands that owner into the same user/org AsyncLocalStorage context the
8077
- // foreground request uses, so credential and data scoping stay aligned.
8078
- await seedBackgroundAgentRunOwnerContext(event, prepared.runId);
8079
-
8080
8059
  try {
8060
+ // DIAGNOSTIC: load the run-store diagnostic recorder only after the
8061
+ // authenticated marker has been mirrored into globalThis. run-store
8062
+ // can initialize the DB pool; the pool must see the same background
8063
+ // proof as the agent timeout logic before that happens.
8064
+ const diag = await import("../agent/run-store.js")
8065
+ .then((m) => ({
8066
+ record: m.recordRunDiagnostic,
8067
+ stages: m.RUN_DIAG_STAGE,
8068
+ }))
8069
+ .catch(() => null);
8070
+
8071
+ // Record "the route handler was entered" against the run after auth
8072
+ // succeeds. This is the proof the bg-fn invocation actually reached
8073
+ // Nitro (vs. dying at the function entry / never being invoked).
8074
+ if (diag) {
8075
+ await diag
8076
+ .record(prepared.runId, diag.stages.routeEntered)
8077
+ .catch(() => {});
8078
+ await diag
8079
+ .record(prepared.runId, diag.stages.authPassed)
8080
+ .catch(() => {});
8081
+ }
8082
+
8083
+ // Stash the verified+augmented body for the handler — the body stream
8084
+ // is already consumed, so the handler reads this instead.
8085
+ (event as any).context = (event as any).context ?? {};
8086
+ (event as any).context.__agentChatBackgroundBody = prepared.body;
8087
+
8088
+ // Durable owner context: this self-dispatch is cookieless (HMAC-only).
8089
+ // Resolve the owner from the persisted run row, never the request
8090
+ // body, then invoke the normal handler. The shared agent-run context
8091
+ // helper expands that owner into the same user/org AsyncLocalStorage
8092
+ // context the foreground request uses, so credential and data scoping
8093
+ // stay aligned.
8094
+ await seedBackgroundAgentRunOwnerContext(event, prepared.runId);
8081
8095
  return await invokeAgentChatHandler(event);
8082
8096
  } catch (err: any) {
8083
8097
  console.error("[agent-chat] _process-run failed:", err);
@@ -8099,6 +8113,18 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
8099
8113
  );
8100
8114
  setResponseStatus(event, 500);
8101
8115
  return { error: "process-run failed" };
8116
+ } finally {
8117
+ if (expectsBackgroundRuntime) {
8118
+ if (hadExpectedRuntimeMarker) {
8119
+ runtimeGlobals.__AGENT_NATIVE_BACKGROUND_RUNTIME_EXPECTED__ =
8120
+ previousExpectedRuntimeMarker;
8121
+ } else {
8122
+ Reflect.deleteProperty(
8123
+ runtimeGlobals,
8124
+ "__AGENT_NATIVE_BACKGROUND_RUNTIME_EXPECTED__",
8125
+ );
8126
+ }
8127
+ }
8102
8128
  }
8103
8129
  }),
8104
8130
  );
@@ -228,18 +228,25 @@ export function DashboardFilterBar({
228
228
  <Collapsible
229
229
  open={filtersOpen}
230
230
  onOpenChange={setFiltersOpen}
231
- className="group rounded-lg border border-border bg-card p-3"
231
+ className="group rounded-lg border border-border bg-card px-3 py-2"
232
232
  >
233
- <div className="flex items-center justify-between gap-3">
234
- <div className="flex items-baseline gap-2">
235
- <h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
236
- {t("sqlDashboard.filters")}
237
- </h3>
238
- <span className="text-[10px] text-muted-foreground/60">
239
- {t("sqlDashboard.autoApplied")}
240
- </span>
241
- </div>
242
- <div className="flex items-center gap-1 opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100 group-data-[state=closed]:opacity-100">
233
+ <div className="flex flex-wrap items-end justify-between gap-3">
234
+ <CollapsibleContent className="min-w-0 flex-1">
235
+ <div className="flex flex-wrap gap-3 items-end">
236
+ {uniqueFilters.map((f) => (
237
+ <FilterControl
238
+ key={f.id}
239
+ filter={f}
240
+ vars={vars}
241
+ hasParam={(key) =>
242
+ searchParams.has(FILTER_PARAM_PREFIX + key)
243
+ }
244
+ setValue={(updates) => setParam(updates)}
245
+ />
246
+ ))}
247
+ </div>
248
+ </CollapsibleContent>
249
+ <div className="flex shrink-0 items-center gap-1 self-center opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100 group-data-[state=closed]:opacity-100">
243
250
  {onSaveView && filtersActive && (
244
251
  <Button
245
252
  variant="ghost"
@@ -284,19 +291,6 @@ export function DashboardFilterBar({
284
291
  </CollapsibleTrigger>
285
292
  </div>
286
293
  </div>
287
- <CollapsibleContent className="pt-3">
288
- <div className="flex flex-wrap gap-3 items-end">
289
- {uniqueFilters.map((f) => (
290
- <FilterControl
291
- key={f.id}
292
- filter={f}
293
- vars={vars}
294
- hasParam={(key) => searchParams.has(FILTER_PARAM_PREFIX + key)}
295
- setValue={(updates) => setParam(updates)}
296
- />
297
- ))}
298
- </div>
299
- </CollapsibleContent>
300
294
  </Collapsible>
301
295
 
302
296
  <Dialog open={saveDialogOpen} onOpenChange={setSaveDialogOpen}>
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Dashboard charts no longer show future dates when clients send bad event timestamps.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Dashboard filters take less space and keep view actions tucked to the side.
@@ -50,6 +50,10 @@ function nowIso(): string {
50
50
  return new Date().toISOString();
51
51
  }
52
52
 
53
+ function todayIsoDate(): string {
54
+ return nowIso().slice(0, 10);
55
+ }
56
+
53
57
  function randomHex(bytes: number): string {
54
58
  const arr = new Uint8Array(bytes);
55
59
  if (!globalThis.crypto?.getRandomValues) {
@@ -204,17 +208,30 @@ function asString(value: unknown): string | null {
204
208
  return null;
205
209
  }
206
210
 
207
- function normalizeTimestamp(value: unknown): string {
208
- if (value instanceof Date) return value.toISOString();
211
+ export function normalizeAnalyticsTimestamp(
212
+ value: unknown,
213
+ receivedAt = nowIso(),
214
+ ): string {
215
+ const fallback = (() => {
216
+ const date = new Date(receivedAt);
217
+ return Number.isNaN(date.getTime()) ? nowIso() : date.toISOString();
218
+ })();
219
+ const fallbackTime = new Date(fallback).getTime();
220
+ const normalize = (date: Date) => {
221
+ if (Number.isNaN(date.getTime())) return fallback;
222
+ return date.getTime() > fallbackTime ? fallback : date.toISOString();
223
+ };
224
+
225
+ if (value instanceof Date) return normalize(value);
209
226
  if (typeof value === "number") {
210
227
  const d = new Date(value);
211
- return Number.isNaN(d.getTime()) ? nowIso() : d.toISOString();
228
+ return normalize(d);
212
229
  }
213
230
  if (typeof value === "string" && value.trim()) {
214
231
  const d = new Date(value);
215
- return Number.isNaN(d.getTime()) ? nowIso() : d.toISOString();
232
+ return normalize(d);
216
233
  }
217
- return nowIso();
234
+ return fallback;
218
235
  }
219
236
 
220
237
  function eventDateFromTimestamp(timestamp: string): string {
@@ -362,7 +379,7 @@ export async function recordAnalyticsEvents(
362
379
  asString((properties as any).anonymousId) ??
363
380
  asString((properties as any).distinctId);
364
381
  const userKey = userId || anonymousId;
365
- const timestamp = normalizeTimestamp(event.timestamp);
382
+ const timestamp = normalizeAnalyticsTimestamp(event.timestamp, receivedAt);
366
383
 
367
384
  return {
368
385
  id: id("evt"),
@@ -516,9 +533,17 @@ function scopeClause(scope: AnalyticsScope): {
516
533
  };
517
534
  }
518
535
 
519
- function scopedAnalyticsSql(
536
+ function freshnessClause(tableName: string): string {
537
+ if (tableName === "analytics_events") {
538
+ return "(COALESCE(NULLIF(event_date, ''), substr(timestamp, 1, 10)) <= ?)";
539
+ }
540
+ return "(substr(started_at, 1, 10) <= ?)";
541
+ }
542
+
543
+ export function scopedAnalyticsSql(
520
544
  sql: string,
521
545
  scope: AnalyticsScope,
546
+ today = todayIsoDate(),
522
547
  ): { sql: string; args: Array<string | null> } {
523
548
  const args: Array<string | null> = [];
524
549
  const aliasRe =
@@ -536,8 +561,8 @@ function scopedAnalyticsSql(
536
561
  ? aliasPart
537
562
  : ` AS ${normalizedTable}`;
538
563
  const scopeDef = scopeClause(scope);
539
- args.push(...scopeDef.args);
540
- return `${keyword} (SELECT * FROM ${normalizedTable} WHERE ${scopeDef.sql})${usableAlias}`;
564
+ args.push(...scopeDef.args, today);
565
+ return `${keyword} (SELECT * FROM ${normalizedTable} WHERE ${scopeDef.sql} AND ${freshnessClause(normalizedTable)})${usableAlias}`;
541
566
  },
542
567
  );
543
568
  return { sql: rewritten, args };
@@ -128,6 +128,10 @@ function daysAgoSql(days: number): string {
128
128
  return `to_char(CURRENT_DATE - INTERVAL '${days} ${unit}', 'YYYY-MM-DD')`;
129
129
  }
130
130
 
131
+ function todaySql(): string {
132
+ return "to_char(CURRENT_DATE, 'YYYY-MM-DD')";
133
+ }
134
+
131
135
  function windowStartFilter(days: number): string {
132
136
  return `${EVENT_DATE_SQL} >= ${daysAgoSql(days)}`;
133
137
  }
@@ -140,14 +144,14 @@ function rollingWindowStartSql(
140
144
  }
141
145
 
142
146
  function dashboardTimeRangeFilter(dateExpr = EVENT_DATE_FILTER_SQL): string {
143
- return `('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND ${dateExpr} >= ${daysAgoSql(7)}) OR ('{{timeRange}}' = '30d' AND ${dateExpr} >= ${daysAgoSql(30)}) OR ('{{timeRange}}' = '90d' AND ${dateExpr} >= ${daysAgoSql(90)}) OR ('{{timeRange}}' = '180d' AND ${dateExpr} >= ${daysAgoSql(180)}) OR ('{{timeRange}}' = '365d' AND ${dateExpr} >= ${daysAgoSql(365)}))`;
147
+ return `(${dateExpr} <= ${todaySql()} AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND ${dateExpr} >= ${daysAgoSql(7)}) OR ('{{timeRange}}' = '30d' AND ${dateExpr} >= ${daysAgoSql(30)}) OR ('{{timeRange}}' = '90d' AND ${dateExpr} >= ${daysAgoSql(90)}) OR ('{{timeRange}}' = '180d' AND ${dateExpr} >= ${daysAgoSql(180)}) OR ('{{timeRange}}' = '365d' AND ${dateExpr} >= ${daysAgoSql(365)})))`;
144
148
  }
145
149
 
146
150
  function dashboardLookbackTimeRangeFilter(
147
151
  dateExpr = EVENT_DATE_FILTER_SQL,
148
152
  lookbackDays = 0,
149
153
  ): string {
150
- return `('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND ${dateExpr} >= ${daysAgoSql(7 + lookbackDays)}) OR ('{{timeRange}}' = '30d' AND ${dateExpr} >= ${daysAgoSql(30 + lookbackDays)}) OR ('{{timeRange}}' = '90d' AND ${dateExpr} >= ${daysAgoSql(90 + lookbackDays)}) OR ('{{timeRange}}' = '180d' AND ${dateExpr} >= ${daysAgoSql(180 + lookbackDays)}) OR ('{{timeRange}}' = '365d' AND ${dateExpr} >= ${daysAgoSql(365 + lookbackDays)}))`;
154
+ return `(${dateExpr} <= ${todaySql()} AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND ${dateExpr} >= ${daysAgoSql(7 + lookbackDays)}) OR ('{{timeRange}}' = '30d' AND ${dateExpr} >= ${daysAgoSql(30 + lookbackDays)}) OR ('{{timeRange}}' = '90d' AND ${dateExpr} >= ${daysAgoSql(90 + lookbackDays)}) OR ('{{timeRange}}' = '180d' AND ${dateExpr} >= ${daysAgoSql(180 + lookbackDays)}) OR ('{{timeRange}}' = '365d' AND ${dateExpr} >= ${daysAgoSql(365 + lookbackDays)})))`;
151
155
  }
152
156
 
153
157
  const DASHBOARD_TIME_RANGE_FILTER = dashboardTimeRangeFilter();