@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +17 -0
- package/corpus/core/package.json +2 -2
- package/corpus/core/src/agent/durable-background.ts +6 -0
- package/corpus/core/src/agent/engine/builder-engine.ts +7 -3
- package/corpus/core/src/agent/production-agent.ts +10 -3
- package/corpus/core/src/client/AssistantChat.tsx +30 -4
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +11 -3
- package/corpus/core/src/client/agent-chat.ts +18 -3
- package/corpus/core/src/client/use-agent-engine-configured.ts +7 -4
- package/corpus/core/src/db/client.ts +10 -1
- package/corpus/core/src/server/agent-chat-plugin.ts +69 -43
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +18 -24
- package/corpus/templates/analytics/changelog/2026-07-01-dashboard-charts-no-longer-show-future-dates-when-clients-se.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-01-dashboard-filters-take-less-space-and-keep-view-actions-tucked.md +6 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +34 -9
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +6 -2
- package/corpus/templates/analytics/server/lib/session-replay.ts +109 -50
- package/corpus/templates/analytics/server/plugins/db.ts +21 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/design/app/pages/DesignEditor.tsx +176 -13
- package/corpus/templates/design/changelog/2026-07-01-exporting-a-design-as-png-or-svg-now-downloads-just-the-sele.md +6 -0
- package/dist/agent/durable-background.d.ts +1 -0
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +3 -0
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/engine/builder-engine.js +7 -1
- package/dist/agent/engine/builder-engine.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +6 -4
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +8 -2
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +18 -2
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +7 -3
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts +7 -0
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +5 -1
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
- package/dist/client/use-agent-engine-configured.js +7 -1
- package/dist/client/use-agent-engine-configured.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/db/client.d.ts +6 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +8 -1
- package/dist/db/client.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +6 -6
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +57 -40
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +2 -2
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -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
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.84.
|
|
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
|
-
|
|
197
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
/**
|
|
652
|
-
|
|
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(
|
|
2979
|
+
setComposerContextItem(
|
|
2980
|
+
item: AgentChatContextItem,
|
|
2981
|
+
options?: { focus?: boolean },
|
|
2982
|
+
) {
|
|
2962
2983
|
stageComposerContextItem(item);
|
|
2963
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
869
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
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
|
|
231
|
+
className="group rounded-lg border border-border bg-card px-3 py-2"
|
|
232
232
|
>
|
|
233
|
-
<div className="flex items-
|
|
234
|
-
<
|
|
235
|
-
<
|
|
236
|
-
{
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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}>
|
|
@@ -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
|
|
208
|
-
|
|
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
|
|
228
|
+
return normalize(d);
|
|
212
229
|
}
|
|
213
230
|
if (typeof value === "string" && value.trim()) {
|
|
214
231
|
const d = new Date(value);
|
|
215
|
-
return
|
|
232
|
+
return normalize(d);
|
|
216
233
|
}
|
|
217
|
-
return
|
|
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 =
|
|
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
|
|
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();
|