@agent-native/core 0.131.9 → 0.132.1
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 +2 -2
- package/corpus/core/CHANGELOG.md +105 -0
- package/corpus/core/docs/content/durable-background-runs.mdx +15 -6
- package/corpus/core/docs/design/durable-agent-runs.md +15 -11
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/activity.ts +127 -10
- package/corpus/core/src/a2a/index.ts +3 -0
- package/corpus/core/src/action.ts +52 -11
- package/corpus/core/src/agent/durable-background.ts +38 -28
- package/corpus/core/src/agent/harness/background.ts +12 -2
- package/corpus/core/src/agent/production-agent.ts +210 -43
- package/corpus/core/src/agent/run-store.ts +45 -3
- package/corpus/core/src/application-state/handlers.ts +45 -0
- package/corpus/core/src/application-state/index.ts +3 -0
- package/corpus/core/src/application-state/store.ts +28 -11
- package/corpus/core/src/audit/redact.ts +104 -18
- package/corpus/core/src/client/AgentPanel.tsx +2 -7
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +3 -0
- package/corpus/core/src/client/RunStuckBanner.tsx +29 -6
- package/corpus/core/src/client/agent-chat.ts +4 -9
- package/corpus/core/src/client/application-state.ts +115 -12
- package/corpus/core/src/client/dynamic-suggestions.ts +4 -12
- package/corpus/core/src/client/guided-questions.tsx +12 -23
- package/corpus/core/src/db/client.ts +148 -28
- package/corpus/core/src/db/create-get-db.ts +39 -6
- package/corpus/core/src/db/runtime-diagnostics.ts +41 -6
- package/corpus/core/src/deploy/build.ts +26 -33
- package/corpus/core/src/feature-flags/store.ts +10 -5
- package/corpus/core/src/integrations/a2a-continuations-store.ts +11 -0
- package/corpus/core/src/integrations/config-store.ts +13 -0
- package/corpus/core/src/integrations/google-docs-poller.ts +26 -2
- package/corpus/core/src/mcp-client/routes.ts +33 -2
- package/corpus/core/src/onboarding/plugin.ts +31 -25
- package/corpus/core/src/org/accept-pending.ts +2 -0
- package/corpus/core/src/org/auto-join-domain.ts +2 -0
- package/corpus/core/src/org/context.ts +35 -10
- package/corpus/core/src/org/handlers.ts +6 -0
- package/corpus/core/src/org/request-org-cache.ts +71 -0
- package/corpus/core/src/resources/store.ts +10 -9
- package/corpus/core/src/scripts/runner.ts +6 -3
- package/corpus/core/src/secrets/storage.ts +97 -5
- package/corpus/core/src/server/action-change.ts +33 -0
- package/corpus/core/src/server/action-routes.ts +11 -9
- package/corpus/core/src/server/agent-teams.ts +10 -2
- package/corpus/core/src/server/better-auth-instance.ts +39 -12
- package/corpus/core/src/server/core-routes-plugin.ts +19 -2
- package/corpus/core/src/server/credential-provider.ts +57 -14
- package/corpus/core/src/server/http-response-telemetry.ts +1 -0
- package/corpus/core/src/server/poll.ts +95 -57
- package/corpus/core/src/server/voice-providers-status.ts +19 -4
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +38 -0
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +1 -1
- package/corpus/templates/analytics/actions/ensure-native-v2-dashboards.ts +299 -0
- package/corpus/templates/analytics/actions/list-native-v2-dashboard-templates.ts +34 -0
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +82 -0
- package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +2 -2
- package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +40 -34
- package/corpus/templates/analytics/app/hooks/use-user-pref.ts +3 -3
- package/corpus/templates/analytics/app/i18n-data.ts +40 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +18 -10
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +14 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +1 -0
- package/corpus/templates/analytics/app/routes/chart.tsx +3 -0
- package/corpus/templates/analytics/changelog/2026-07-30-analytics-dashboards-now-expose-native-funnel-heatmap-callou.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-30-analytics-now-ships-reusable-native-v2-dashboard-manifests-w.md +6 -0
- package/corpus/templates/analytics/server/lib/dashboard-report-render.ts +81 -1
- package/corpus/templates/analytics/server/lib/native-v2-dashboards.ts +457 -0
- package/corpus/templates/analytics/server/lib/report-chart-svg.ts +73 -2
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/analytics/shared/dashboard-funnel.ts +83 -0
- package/corpus/templates/content/changelog/2026-07-30-long-running-delegated-requests-now-continue-reliably.md +6 -0
- package/corpus/templates/content/netlify.toml +3 -0
- package/corpus/templates/content/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +2 -1
- package/corpus/templates/slides/actions/patch-deck.ts +25 -1
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +17 -4
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +9 -1
- package/corpus/templates/slides/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/slides/app/context/DeckContext.tsx +11 -3
- package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +39 -11
- package/corpus/templates/slides/app/pages/Index.tsx +2 -2
- package/corpus/templates/slides/changelog/2026-07-30-slide-previews-keep-their-aspect-ratio-isolate-embedded-styl.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-30-slides-no-longer-lose-decks-while-organization-access-is-loa.md +6 -0
- package/corpus/toolkit/CHANGELOG.md +8 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/composer/runtime-adapters.tsx +16 -11
- package/dist/a2a/activity.d.ts +22 -4
- package/dist/a2a/activity.d.ts.map +1 -1
- package/dist/a2a/activity.js +97 -11
- package/dist/a2a/activity.js.map +1 -1
- package/dist/a2a/index.d.ts +1 -1
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js +1 -1
- package/dist/a2a/index.js.map +1 -1
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +41 -11
- package/dist/action.js.map +1 -1
- package/dist/agent/durable-background.d.ts +13 -8
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +36 -28
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/harness/background.d.ts.map +1 -1
- package/dist/agent/harness/background.js +12 -2
- package/dist/agent/harness/background.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +195 -44
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-store.d.ts +2 -0
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +44 -3
- package/dist/agent/run-store.js.map +1 -1
- package/dist/application-state/handlers.d.ts +15 -0
- package/dist/application-state/handlers.d.ts.map +1 -1
- package/dist/application-state/handlers.js +42 -2
- package/dist/application-state/handlers.js.map +1 -1
- package/dist/application-state/index.d.ts +2 -2
- package/dist/application-state/index.d.ts.map +1 -1
- package/dist/application-state/index.js +2 -2
- package/dist/application-state/index.js.map +1 -1
- package/dist/application-state/store.d.ts +11 -0
- package/dist/application-state/store.d.ts.map +1 -1
- package/dist/application-state/store.js +23 -10
- package/dist/application-state/store.js.map +1 -1
- package/dist/audit/redact.d.ts +25 -2
- package/dist/audit/redact.d.ts.map +1 -1
- package/dist/audit/redact.js +85 -21
- package/dist/audit/redact.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +2 -7
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +1 -1
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/RunStuckBanner.d.ts +11 -1
- package/dist/client/RunStuckBanner.d.ts.map +1 -1
- package/dist/client/RunStuckBanner.js +20 -7
- package/dist/client/RunStuckBanner.js.map +1 -1
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +4 -6
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/application-state.d.ts +11 -0
- package/dist/client/application-state.d.ts.map +1 -1
- package/dist/client/application-state.js +87 -9
- package/dist/client/application-state.js.map +1 -1
- package/dist/client/dynamic-suggestions.d.ts.map +1 -1
- package/dist/client/dynamic-suggestions.js +4 -13
- package/dist/client/dynamic-suggestions.js.map +1 -1
- package/dist/client/guided-questions.d.ts.map +1 -1
- package/dist/client/guided-questions.js +6 -22
- package/dist/client/guided-questions.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/client.d.ts +57 -10
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +122 -31
- package/dist/db/client.js.map +1 -1
- package/dist/db/create-get-db.d.ts.map +1 -1
- package/dist/db/create-get-db.js +31 -7
- package/dist/db/create-get-db.js.map +1 -1
- package/dist/db/runtime-diagnostics.d.ts +1 -0
- package/dist/db/runtime-diagnostics.d.ts.map +1 -1
- package/dist/db/runtime-diagnostics.js +31 -6
- package/dist/db/runtime-diagnostics.js.map +1 -1
- package/dist/deploy/build.d.ts +5 -20
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +18 -30
- package/dist/deploy/build.js.map +1 -1
- package/dist/feature-flags/store.d.ts.map +1 -1
- package/dist/feature-flags/store.js +10 -5
- package/dist/feature-flags/store.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
- package/dist/integrations/a2a-continuations-store.js +12 -0
- package/dist/integrations/a2a-continuations-store.js.map +1 -1
- package/dist/integrations/config-store.d.ts +1 -0
- package/dist/integrations/config-store.d.ts.map +1 -1
- package/dist/integrations/config-store.js +11 -0
- package/dist/integrations/config-store.js.map +1 -1
- package/dist/integrations/google-docs-poller.d.ts.map +1 -1
- package/dist/integrations/google-docs-poller.js +19 -2
- package/dist/integrations/google-docs-poller.js.map +1 -1
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
- package/dist/mcp-client/routes.d.ts.map +1 -1
- package/dist/mcp-client/routes.js +30 -2
- package/dist/mcp-client/routes.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +1 -1
- package/dist/onboarding/plugin.d.ts.map +1 -1
- package/dist/onboarding/plugin.js +12 -8
- package/dist/onboarding/plugin.js.map +1 -1
- package/dist/org/accept-pending.d.ts.map +1 -1
- package/dist/org/accept-pending.js +2 -0
- package/dist/org/accept-pending.js.map +1 -1
- package/dist/org/auto-join-domain.d.ts.map +1 -1
- package/dist/org/auto-join-domain.js +2 -0
- package/dist/org/auto-join-domain.js.map +1 -1
- package/dist/org/context.d.ts.map +1 -1
- package/dist/org/context.js +29 -9
- package/dist/org/context.js.map +1 -1
- package/dist/org/handlers.d.ts.map +1 -1
- package/dist/org/handlers.js +5 -0
- package/dist/org/handlers.js.map +1 -1
- package/dist/org/request-org-cache.d.ts +16 -0
- package/dist/org/request-org-cache.d.ts.map +1 -0
- package/dist/org/request-org-cache.js +62 -0
- package/dist/org/request-org-cache.js.map +1 -0
- package/dist/resources/store.d.ts.map +1 -1
- package/dist/resources/store.js +11 -9
- package/dist/resources/store.js.map +1 -1
- package/dist/scripts/runner.d.ts.map +1 -1
- package/dist/scripts/runner.js +3 -3
- package/dist/scripts/runner.js.map +1 -1
- package/dist/secrets/storage.d.ts.map +1 -1
- package/dist/secrets/storage.js +82 -5
- package/dist/secrets/storage.js.map +1 -1
- package/dist/server/action-change.d.ts +20 -0
- package/dist/server/action-change.d.ts.map +1 -1
- package/dist/server/action-change.js +29 -0
- package/dist/server/action-change.js.map +1 -1
- package/dist/server/action-routes.d.ts.map +1 -1
- package/dist/server/action-routes.js +7 -8
- package/dist/server/action-routes.js.map +1 -1
- package/dist/server/agent-teams.d.ts.map +1 -1
- package/dist/server/agent-teams.js +10 -2
- package/dist/server/agent-teams.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +31 -14
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +1 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +18 -4
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/credential-provider.d.ts +16 -0
- package/dist/server/credential-provider.d.ts.map +1 -1
- package/dist/server/credential-provider.js +49 -13
- package/dist/server/credential-provider.js.map +1 -1
- package/dist/server/http-response-telemetry.d.ts.map +1 -1
- package/dist/server/http-response-telemetry.js +1 -0
- package/dist/server/http-response-telemetry.js.map +1 -1
- package/dist/server/poll.d.ts +6 -0
- package/dist/server/poll.d.ts.map +1 -1
- package/dist/server/poll.js +84 -47
- package/dist/server/poll.js.map +1 -1
- package/dist/server/voice-providers-status.d.ts.map +1 -1
- package/dist/server/voice-providers-status.js +12 -3
- package/dist/server/voice-providers-status.js.map +1 -1
- package/docs/content/durable-background-runs.mdx +15 -6
- package/docs/design/durable-agent-runs.md +15 -11
- package/package.json +2 -2
- package/src/a2a/activity.ts +127 -10
- package/src/a2a/index.ts +3 -0
- package/src/action.ts +52 -11
- package/src/agent/durable-background.ts +38 -28
- package/src/agent/harness/background.ts +12 -2
- package/src/agent/production-agent.ts +210 -43
- package/src/agent/run-store.ts +45 -3
- package/src/application-state/handlers.ts +45 -0
- package/src/application-state/index.ts +3 -0
- package/src/application-state/store.ts +28 -11
- package/src/audit/redact.ts +104 -18
- package/src/client/AgentPanel.tsx +2 -7
- package/src/client/MultiTabAssistantChat.tsx +3 -0
- package/src/client/RunStuckBanner.tsx +29 -6
- package/src/client/agent-chat.ts +4 -9
- package/src/client/application-state.ts +115 -12
- package/src/client/dynamic-suggestions.ts +4 -12
- package/src/client/guided-questions.tsx +12 -23
- package/src/db/client.ts +148 -28
- package/src/db/create-get-db.ts +39 -6
- package/src/db/runtime-diagnostics.ts +41 -6
- package/src/deploy/build.ts +26 -33
- package/src/feature-flags/store.ts +10 -5
- package/src/integrations/a2a-continuations-store.ts +11 -0
- package/src/integrations/config-store.ts +13 -0
- package/src/integrations/google-docs-poller.ts +26 -2
- package/src/mcp-client/routes.ts +33 -2
- package/src/onboarding/plugin.ts +31 -25
- package/src/org/accept-pending.ts +2 -0
- package/src/org/auto-join-domain.ts +2 -0
- package/src/org/context.ts +35 -10
- package/src/org/handlers.ts +6 -0
- package/src/org/request-org-cache.ts +71 -0
- package/src/resources/store.ts +10 -9
- package/src/scripts/runner.ts +6 -3
- package/src/secrets/storage.ts +97 -5
- package/src/server/action-change.ts +33 -0
- package/src/server/action-routes.ts +11 -9
- package/src/server/agent-teams.ts +10 -2
- package/src/server/better-auth-instance.ts +39 -12
- package/src/server/core-routes-plugin.ts +19 -2
- package/src/server/credential-provider.ts +57 -14
- package/src/server/http-response-telemetry.ts +1 -0
- package/src/server/poll.ts +95 -57
- package/src/server/voice-providers-status.ts +19 -4
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Durable background agent-chat runs (Netlify background functions).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Enabled by default on deployed Netlify apps. When enabled, a long in-app
|
|
5
|
+
* agent-chat turn is dispatched
|
|
5
6
|
* into a Netlify *background* function (15-min budget) instead of completing
|
|
6
7
|
* synchronously under the ~40s soft-timeout. The foreground POST claims the
|
|
7
8
|
* run slot, inserts the run row, fires an HMAC-signed self-dispatch to
|
|
@@ -18,18 +19,17 @@
|
|
|
18
19
|
* GUARDRAIL: when `isAgentChatDurableBackgroundEnabled()` returns false, the
|
|
19
20
|
* agent-chat handler must behave byte-for-byte like the current synchronous
|
|
20
21
|
* path. The gate is true only when ALL of these hold:
|
|
21
|
-
* 1.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* 1. The runtime is a deployed Netlify app and
|
|
23
|
+
* `AGENT_CHAT_DURABLE_BACKGROUND` is not explicitly disabled, or the
|
|
24
|
+
* existing env/app opt-in path is used on another hosted platform. Netlify
|
|
25
|
+
* deploys emit the background function by default; `false`, `0`, `no`, or
|
|
26
|
+
* `off` disables it.
|
|
26
27
|
* 2. The runtime is hosted/serverless (local dev keeps the inline path so SSE
|
|
27
28
|
* stays a single live stream and no second function is needed).
|
|
28
29
|
* 3. `A2A_SECRET` is configured (the HMAC handoff is required to authenticate
|
|
29
30
|
* the background dispatch; without it the dispatch can't be trusted).
|
|
30
31
|
*
|
|
31
|
-
*
|
|
32
|
-
* proven. And even when enabled, a *dispatch failure degrades to an inline run*:
|
|
32
|
+
* Even when enabled, a *dispatch failure degrades to an inline run*:
|
|
33
33
|
* if the self-dispatch self-POST can't be delivered (fast connection error or
|
|
34
34
|
* fast non-2xx), the foreground handler runs the turn synchronously instead of
|
|
35
35
|
* erroring (see `production-agent.ts` — the inline fallback claims the run row
|
|
@@ -120,7 +120,7 @@ function isNetlifyHostedRuntimeForDispatch(): boolean {
|
|
|
120
120
|
// exposes AWS runtime variables, so keep the function-name fallback for older
|
|
121
121
|
// deploys. Without either check a modern Netlify Function silently selects the
|
|
122
122
|
// portable framework route even though the emitted background function exists.
|
|
123
|
-
if (process.env.SITE_ID) return true; // guard:allow-env-credential
|
|
123
|
+
if (process.env.SITE_ID) return true; // guard:allow-env-credential - Netlify's read-only public site identifier is a runtime host marker, not a user credential.
|
|
124
124
|
// Non-Netlify AWS falls back inline if the /.netlify/functions dispatch
|
|
125
125
|
// fast-fails.
|
|
126
126
|
return Boolean(process.env.AWS_LAMBDA_FUNCTION_NAME);
|
|
@@ -186,8 +186,9 @@ export function dispatchPathTargetsNetlifyBackgroundFunction(
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
/**
|
|
189
|
-
* Env flag for durable background runs.
|
|
190
|
-
*
|
|
189
|
+
* Env flag for durable background runs. On Netlify, unset means enabled and an
|
|
190
|
+
* explicit falsy value opts out. On other hosted platforms, apps still opt in
|
|
191
|
+
* with an explicit truthy value (`true`/`1`/`yes`/`on`).
|
|
191
192
|
*/
|
|
192
193
|
export const AGENT_CHAT_DURABLE_BACKGROUND_ENV =
|
|
193
194
|
"AGENT_CHAT_DURABLE_BACKGROUND";
|
|
@@ -233,6 +234,20 @@ export function isHostedRuntimeForDurableBackground(): boolean {
|
|
|
233
234
|
);
|
|
234
235
|
}
|
|
235
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Netlify is the only host with the emitted 15-minute background function.
|
|
239
|
+
* Keep the framework-wide default scoped to deployed Netlify rather than
|
|
240
|
+
* silently enabling it on other hosted runtimes that may not have that worker.
|
|
241
|
+
*/
|
|
242
|
+
export function isNetlifyHostedRuntimeForDurableBackground(): boolean {
|
|
243
|
+
if (process.env.NETLIFY_LOCAL === "true") return false;
|
|
244
|
+
if (process.env.NETLIFY === "false") return false;
|
|
245
|
+
return Boolean(
|
|
246
|
+
(process.env.NETLIFY && process.env.NETLIFY !== "false") ||
|
|
247
|
+
process.env.SITE_ID, // guard:allow-env-credential - Netlify's read-only public site identifier is a runtime host marker, not a user credential.
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
236
251
|
/**
|
|
237
252
|
* True when THIS process is actually executing inside a Netlify *background*
|
|
238
253
|
* function (the long, 15-min-budget async function whose deployed name ends in
|
|
@@ -353,16 +368,10 @@ export function isDurableBackgroundFlagEnabled(): boolean {
|
|
|
353
368
|
// can statically verify it against the allowlisted `AGENT_*` prefix. Keep this
|
|
354
369
|
// in sync with AGENT_CHAT_DURABLE_BACKGROUND_ENV.
|
|
355
370
|
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
// not reliably baked into a given deploy. So an unset/empty/unknown flag means
|
|
361
|
-
// OFF; an app opts IN only with an explicit truthy value
|
|
362
|
-
// (AGENT_CHAT_DURABLE_BACKGROUND=true). This still composes with the hosted +
|
|
363
|
-
// A2A_SECRET gates below. Flip back to default-on only after the 15-min
|
|
364
|
-
// background-function worker is verified live in production (see the
|
|
365
|
-
// project_durable_bg_prod_verified memory).
|
|
371
|
+
// This parses only the explicit opt-in signal. Netlify's default-on behavior
|
|
372
|
+
// is composed separately in isAgentChatDurableBackgroundEnabled, while other
|
|
373
|
+
// hosted runtimes still require this truthy value. Empty and unknown values
|
|
374
|
+
// remain false so an explicit host opt-in cannot be inferred accidentally.
|
|
366
375
|
const raw = process.env.AGENT_CHAT_DURABLE_BACKGROUND;
|
|
367
376
|
if (raw == null) return false;
|
|
368
377
|
const normalized = raw.trim().toLowerCase();
|
|
@@ -387,12 +396,10 @@ export function isDurableBackgroundFlagExplicitlyDisabled(): boolean {
|
|
|
387
396
|
}
|
|
388
397
|
|
|
389
398
|
/**
|
|
390
|
-
* The single gate.
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
* current synchronous behavior is used unchanged.
|
|
394
|
-
* opt-ins deliberately require the env flag too because that flag controls
|
|
395
|
-
* whether the `server-agent-background` function exists in the deploy output.
|
|
399
|
+
* The single gate. On deployed Netlify, durable runs are enabled unless the env
|
|
400
|
+
* flag is explicitly falsy. Other hosted runtimes retain the explicit env/app
|
|
401
|
+
* opt-in path. In every case the runtime must be hosted and have A2A_SECRET.
|
|
402
|
+
* False means the current synchronous behavior is used unchanged.
|
|
396
403
|
*/
|
|
397
404
|
export function isAgentChatDurableBackgroundEnabled(options?: {
|
|
398
405
|
appOptIn?: boolean;
|
|
@@ -404,12 +411,15 @@ export function isAgentChatDurableBackgroundEnabled(options?: {
|
|
|
404
411
|
// prevent.
|
|
405
412
|
if (options?.appOptIn === false) return false;
|
|
406
413
|
const envOptIn = isDurableBackgroundFlagEnabled();
|
|
414
|
+
const netlifyDefaultOptIn =
|
|
415
|
+
isNetlifyHostedRuntimeForDurableBackground() &&
|
|
416
|
+
!isDurableBackgroundFlagExplicitlyDisabled();
|
|
407
417
|
const workspaceAppOptIn =
|
|
408
418
|
options?.appOptIn === true &&
|
|
409
419
|
!isDurableBackgroundFlagExplicitlyDisabled() &&
|
|
410
420
|
resolveWorkspaceBackgroundFunctionUrlPath() !== null;
|
|
411
421
|
return (
|
|
412
|
-
(envOptIn || workspaceAppOptIn) &&
|
|
422
|
+
(envOptIn || netlifyDefaultOptIn || workspaceAppOptIn) &&
|
|
413
423
|
isHostedRuntimeForDurableBackground() &&
|
|
414
424
|
hasConfiguredA2ASecret()
|
|
415
425
|
);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { redactArgsToValue, redactTextToSummary } from "../../audit/redact.js";
|
|
1
2
|
import type {
|
|
2
3
|
BackgroundAgentControlInput,
|
|
3
4
|
BackgroundAgentControlResult,
|
|
@@ -233,13 +234,22 @@ function summarizeAgentChatEvent(event: AgentChatEvent): {
|
|
|
233
234
|
return {
|
|
234
235
|
kind: "status",
|
|
235
236
|
message: `Running ${event.tool}`,
|
|
236
|
-
metadata: {
|
|
237
|
+
metadata: {
|
|
238
|
+
type: "tool_start",
|
|
239
|
+
tool: event.tool,
|
|
240
|
+
input: redactArgsToValue(event.input),
|
|
241
|
+
},
|
|
237
242
|
};
|
|
238
243
|
case "tool_done":
|
|
239
244
|
return {
|
|
240
245
|
kind: "artifact",
|
|
241
246
|
message: event.result,
|
|
242
|
-
metadata: {
|
|
247
|
+
metadata: {
|
|
248
|
+
type: "tool_done",
|
|
249
|
+
tool: event.tool,
|
|
250
|
+
input: redactArgsToValue(event.input),
|
|
251
|
+
result: redactTextToSummary(event.result),
|
|
252
|
+
},
|
|
243
253
|
};
|
|
244
254
|
case "error":
|
|
245
255
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
|
|
3
|
-
import Ajv, { type ValidateFunction } from "ajv";
|
|
3
|
+
import Ajv, { type ErrorObject, type ValidateFunction } from "ajv";
|
|
4
4
|
import {
|
|
5
5
|
defineEventHandler,
|
|
6
6
|
getHeader,
|
|
@@ -3596,6 +3596,9 @@ const rawToolInputAjv = new Ajv({
|
|
|
3596
3596
|
coerceTypes: true,
|
|
3597
3597
|
useDefaults: false,
|
|
3598
3598
|
removeAdditional: false,
|
|
3599
|
+
// `parentSchema`/`data` on each error are what let us drop the branches of a
|
|
3600
|
+
// `oneOf` the caller never meant to match.
|
|
3601
|
+
verbose: true,
|
|
3599
3602
|
});
|
|
3600
3603
|
|
|
3601
3604
|
const rawToolInputValidatorCache = new WeakMap<object, ValidateFunction>();
|
|
@@ -3636,17 +3639,83 @@ function isStructurallyEmptyToolValue(value: unknown): boolean {
|
|
|
3636
3639
|
);
|
|
3637
3640
|
}
|
|
3638
3641
|
|
|
3642
|
+
function compileToolValueValidator(schema: object): ValidateFunction | null {
|
|
3643
|
+
const cached = optionalPlaceholderValidatorCache.get(schema);
|
|
3644
|
+
if (cached) return cached;
|
|
3645
|
+
try {
|
|
3646
|
+
const validator = optionalPlaceholderAjv.compile(schema);
|
|
3647
|
+
optionalPlaceholderValidatorCache.set(schema, validator);
|
|
3648
|
+
return validator;
|
|
3649
|
+
} catch {
|
|
3650
|
+
return null;
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3639
3654
|
function schemaAcceptsToolValue(schema: object, value: unknown): boolean {
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3655
|
+
const validator = compileToolValueValidator(schema);
|
|
3656
|
+
return validator ? Boolean(validator(value)) : false;
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
/**
|
|
3660
|
+
* True only when the sub-schema compiled AND rejected the value. A sub-schema
|
|
3661
|
+
* that cannot be compiled standalone — a `$ref` into the root `$defs`, say — is
|
|
3662
|
+
* unknown, not invalid; counting it as invalid would let one unreadable
|
|
3663
|
+
* sub-schema delete the caller's intentional empty values everywhere else in
|
|
3664
|
+
* the same object.
|
|
3665
|
+
*/
|
|
3666
|
+
function schemaRejectsToolValue(schema: object, value: unknown): boolean {
|
|
3667
|
+
const validator = compileToolValueValidator(schema);
|
|
3668
|
+
return validator ? !validator(value) : false;
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
/** The `const`/single-value-`enum` a discriminated-union branch pins a key to. */
|
|
3672
|
+
function schemaDiscriminatorValue(
|
|
3673
|
+
schema: AgentNativeJsonSchema | undefined,
|
|
3674
|
+
): unknown {
|
|
3675
|
+
if (!schema) return undefined;
|
|
3676
|
+
if (schema.const !== undefined) return schema.const;
|
|
3677
|
+
if (Array.isArray(schema.enum) && schema.enum.length === 1) {
|
|
3678
|
+
return schema.enum[0];
|
|
3648
3679
|
}
|
|
3649
|
-
return
|
|
3680
|
+
return undefined;
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
/**
|
|
3684
|
+
* Index of the `oneOf`/`anyOf` branch whose discriminator constants all match
|
|
3685
|
+
* `value`, or -1 when the union is not discriminated or nothing matches.
|
|
3686
|
+
*/
|
|
3687
|
+
function discriminatedBranchIndex(
|
|
3688
|
+
branches: readonly AgentNativeJsonSchema[],
|
|
3689
|
+
value: unknown,
|
|
3690
|
+
): number {
|
|
3691
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return -1;
|
|
3692
|
+
const record = value as Record<string, unknown>;
|
|
3693
|
+
return branches.findIndex((branch) => {
|
|
3694
|
+
const properties = branch.properties;
|
|
3695
|
+
if (!properties) return false;
|
|
3696
|
+
const discriminators = Object.entries(properties).filter(
|
|
3697
|
+
([, spec]) => schemaDiscriminatorValue(spec) !== undefined,
|
|
3698
|
+
);
|
|
3699
|
+
if (discriminators.length === 0) return false;
|
|
3700
|
+
return discriminators.every(
|
|
3701
|
+
([key, spec]) => record[key] === schemaDiscriminatorValue(spec),
|
|
3702
|
+
);
|
|
3703
|
+
});
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
/**
|
|
3707
|
+
* The object schema that actually governs `value` — the union branch its
|
|
3708
|
+
* discriminator selects, or the schema itself. Undefined when a union cannot be
|
|
3709
|
+
* resolved, since guessing a branch would strip the wrong keys.
|
|
3710
|
+
*/
|
|
3711
|
+
function resolveObjectSchemaBranch(
|
|
3712
|
+
schema: RawJsonSchema,
|
|
3713
|
+
value: unknown,
|
|
3714
|
+
): RawJsonSchema | undefined {
|
|
3715
|
+
const branches = schema.oneOf ?? schema.anyOf;
|
|
3716
|
+
if (!branches?.length) return schema;
|
|
3717
|
+
const index = discriminatedBranchIndex(branches, value);
|
|
3718
|
+
return index >= 0 ? branches[index] : undefined;
|
|
3650
3719
|
}
|
|
3651
3720
|
|
|
3652
3721
|
/**
|
|
@@ -3654,35 +3723,82 @@ function schemaAcceptsToolValue(schema: object, value: unknown): boolean {
|
|
|
3654
3723
|
* instead of omitting it. One schema-invalid empty value proves that pattern;
|
|
3655
3724
|
* only then strip the other optional empty/default sentinels from the call.
|
|
3656
3725
|
* Calls whose empty values are all schema-valid keep their intentional clears.
|
|
3726
|
+
*
|
|
3727
|
+
* The proof is evaluated per object, and the walk descends through array items
|
|
3728
|
+
* and discriminated-union branches: gateways pre-fill the leaf object they are
|
|
3729
|
+
* building (`operations[0].fields`), which a top-level-only sweep never reaches.
|
|
3657
3730
|
*/
|
|
3658
|
-
function
|
|
3731
|
+
function stripOptionalToolPlaceholders(
|
|
3659
3732
|
schema: RawJsonSchema | undefined,
|
|
3660
|
-
|
|
3661
|
-
): {
|
|
3662
|
-
if (!schema
|
|
3663
|
-
|
|
3733
|
+
value: unknown,
|
|
3734
|
+
): { value: unknown; changed: boolean } {
|
|
3735
|
+
if (!schema) return { value, changed: false };
|
|
3736
|
+
|
|
3737
|
+
if (Array.isArray(value)) {
|
|
3738
|
+
const itemSchema = schema.items;
|
|
3739
|
+
if (!itemSchema) return { value, changed: false };
|
|
3740
|
+
let changed = false;
|
|
3741
|
+
const items = value.map((item) => {
|
|
3742
|
+
const result = stripOptionalToolPlaceholders(itemSchema, item);
|
|
3743
|
+
if (result.changed) changed = true;
|
|
3744
|
+
return result.value;
|
|
3745
|
+
});
|
|
3746
|
+
return changed
|
|
3747
|
+
? { value: items, changed: true }
|
|
3748
|
+
: { value, changed: false };
|
|
3664
3749
|
}
|
|
3665
|
-
|
|
3750
|
+
|
|
3751
|
+
if (!value || typeof value !== "object") return { value, changed: false };
|
|
3752
|
+
|
|
3753
|
+
const objectSchema = resolveObjectSchemaBranch(schema, value);
|
|
3754
|
+
const properties = objectSchema?.properties;
|
|
3755
|
+
if (!properties) return { value, changed: false };
|
|
3666
3756
|
|
|
3667
3757
|
const required = new Set(
|
|
3668
|
-
Array.isArray(
|
|
3758
|
+
Array.isArray(objectSchema.required) ? objectSchema.required : [],
|
|
3669
3759
|
);
|
|
3760
|
+
const record = value as Record<string, unknown>;
|
|
3670
3761
|
const placeholders: string[] = [];
|
|
3671
3762
|
let hasSchemaInvalidPlaceholder = false;
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3763
|
+
let normalized: Record<string, unknown> | null = null;
|
|
3764
|
+
|
|
3765
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
3766
|
+
const propertySchema = properties[key];
|
|
3675
3767
|
if (!propertySchema || typeof propertySchema !== "object") continue;
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3768
|
+
if (!required.has(key) && isStructurallyEmptyToolValue(entry)) {
|
|
3769
|
+
placeholders.push(key);
|
|
3770
|
+
if (schemaRejectsToolValue(propertySchema, entry)) {
|
|
3771
|
+
hasSchemaInvalidPlaceholder = true;
|
|
3772
|
+
}
|
|
3773
|
+
continue;
|
|
3679
3774
|
}
|
|
3775
|
+
const nested = stripOptionalToolPlaceholders(propertySchema, entry);
|
|
3776
|
+
if (nested.changed) {
|
|
3777
|
+
normalized ??= { ...record };
|
|
3778
|
+
normalized[key] = nested.value;
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3782
|
+
if (hasSchemaInvalidPlaceholder) {
|
|
3783
|
+
normalized ??= { ...record };
|
|
3784
|
+
for (const key of placeholders) delete normalized[key];
|
|
3680
3785
|
}
|
|
3681
|
-
if (!hasSchemaInvalidPlaceholder) return { input, changed: false };
|
|
3682
3786
|
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3787
|
+
return normalized
|
|
3788
|
+
? { value: normalized, changed: true }
|
|
3789
|
+
: { value, changed: false };
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
function normalizeOptionalToolPlaceholders(
|
|
3793
|
+
schema: RawJsonSchema | undefined,
|
|
3794
|
+
input: unknown,
|
|
3795
|
+
): { input: unknown; changed: boolean } {
|
|
3796
|
+
if (!schema?.properties || !input || typeof input !== "object") {
|
|
3797
|
+
return { input, changed: false };
|
|
3798
|
+
}
|
|
3799
|
+
if (Array.isArray(input)) return { input, changed: false };
|
|
3800
|
+
const result = stripOptionalToolPlaceholders(schema, input);
|
|
3801
|
+
return { input: result.value, changed: result.changed };
|
|
3686
3802
|
}
|
|
3687
3803
|
|
|
3688
3804
|
/**
|
|
@@ -3754,6 +3870,54 @@ function shouldValidateRawToolParameters(entry: ActionEntry): boolean {
|
|
|
3754
3870
|
return !maybeSchema?.["~standard"] && Boolean(entry.tool.parameters);
|
|
3755
3871
|
}
|
|
3756
3872
|
|
|
3873
|
+
/**
|
|
3874
|
+
* With `allErrors`, a failing union reports every branch, so a five-branch
|
|
3875
|
+
* union answers "your `patch-deck-fields` op has a bad enum" with eight
|
|
3876
|
+
* complaints about `slideId`/`orderedIds` from the four branches the caller
|
|
3877
|
+
* never meant. The model reads the loudest, wrong advice and re-sends the same
|
|
3878
|
+
* arguments until the identical-error breaker ends the turn. When the
|
|
3879
|
+
* discriminator names a branch, report only that branch's errors.
|
|
3880
|
+
*
|
|
3881
|
+
* Both union keywords are load-bearing for the same Zod schema: Zod v4's own
|
|
3882
|
+
* `toJSONSchema` emits `oneOf` for a discriminated union, while the manual
|
|
3883
|
+
* fallback converter in `action.ts` emits `anyOf`.
|
|
3884
|
+
*
|
|
3885
|
+
* Scoped by `instancePath` as well as `schemaPath`: every element of an array
|
|
3886
|
+
* shares one `items` schema, so `operations[0]` and `operations[1]` produce
|
|
3887
|
+
* branch errors under the same `schemaPath` and only the instance path says
|
|
3888
|
+
* which element they belong to.
|
|
3889
|
+
*/
|
|
3890
|
+
function isWithinInstancePath(candidate: string, root: string): boolean {
|
|
3891
|
+
return candidate === root || candidate.startsWith(`${root}/`);
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
function narrowUnionBranchErrors(
|
|
3895
|
+
errors: ErrorObject[] | null | undefined,
|
|
3896
|
+
): ErrorObject[] | null | undefined {
|
|
3897
|
+
if (!errors?.length) return errors;
|
|
3898
|
+
let kept = errors;
|
|
3899
|
+
for (const error of errors) {
|
|
3900
|
+
const keyword = error.keyword;
|
|
3901
|
+
if (keyword !== "oneOf" && keyword !== "anyOf") continue;
|
|
3902
|
+
const branches = (error.parentSchema as RawJsonSchema | undefined)?.[
|
|
3903
|
+
keyword
|
|
3904
|
+
];
|
|
3905
|
+
if (!branches?.length) continue;
|
|
3906
|
+
const index = discriminatedBranchIndex(branches, error.data);
|
|
3907
|
+
if (index < 0) continue;
|
|
3908
|
+
const branchPrefix = `${error.schemaPath}/`;
|
|
3909
|
+
kept = kept.filter((candidate) => {
|
|
3910
|
+
if (candidate === error) return false;
|
|
3911
|
+
if (!isWithinInstancePath(candidate.instancePath, error.instancePath)) {
|
|
3912
|
+
return true;
|
|
3913
|
+
}
|
|
3914
|
+
if (!candidate.schemaPath.startsWith(branchPrefix)) return true;
|
|
3915
|
+
return candidate.schemaPath.startsWith(`${branchPrefix}${index}/`);
|
|
3916
|
+
});
|
|
3917
|
+
}
|
|
3918
|
+
return kept.length ? kept : errors;
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3757
3921
|
function validateRawToolInput(
|
|
3758
3922
|
entry: ActionEntry,
|
|
3759
3923
|
input: unknown,
|
|
@@ -3768,7 +3932,7 @@ function validateRawToolInput(
|
|
|
3768
3932
|
return `tool schema is invalid: ${sanitizeToolErrorValue(err)}`;
|
|
3769
3933
|
}
|
|
3770
3934
|
if (validator(input === undefined ? {} : input)) return null;
|
|
3771
|
-
return rawToolInputAjv.errorsText(validator.errors, {
|
|
3935
|
+
return rawToolInputAjv.errorsText(narrowUnionBranchErrors(validator.errors), {
|
|
3772
3936
|
separator: "; ",
|
|
3773
3937
|
dataVar: "input",
|
|
3774
3938
|
});
|
|
@@ -5673,23 +5837,26 @@ export async function runAgentLoop(opts: {
|
|
|
5673
5837
|
result = `${result}\n\n${formatAgentWarningsForToolResult(agentWarnings)}`;
|
|
5674
5838
|
}
|
|
5675
5839
|
|
|
5676
|
-
// Auto-refresh the UI after a successful mutating tool call. Any
|
|
5677
|
-
// that isn't
|
|
5678
|
-
//
|
|
5679
|
-
//
|
|
5680
|
-
//
|
|
5681
|
-
// remember to call
|
|
5682
|
-
|
|
5840
|
+
// Auto-refresh the UI after a successful mutating tool call. Any call
|
|
5841
|
+
// that isn't read-only — by its own per-call Plan-mode effect, else the
|
|
5842
|
+
// action's readOnly flag — is assumed to mutate. The client's useDbSync
|
|
5843
|
+
// listener sees a change event with source:"action" and invalidates
|
|
5844
|
+
// ["action"] queries so list-* / get-* refetch. This makes refresh after
|
|
5845
|
+
// agent writes reliable without the model needing to remember to call
|
|
5846
|
+
// `refresh-screen` itself.
|
|
5847
|
+
if (!isError) {
|
|
5683
5848
|
try {
|
|
5684
|
-
const { notifyActionChange } =
|
|
5849
|
+
const { actionCallIsReadOnly, notifyActionChange } =
|
|
5685
5850
|
await import("../server/action-change.js");
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5851
|
+
if (!actionCallIsReadOnly(actionEntry, toolCall.input, false)) {
|
|
5852
|
+
const owner = opts.ownerEmail ?? getRequestUserEmail() ?? undefined;
|
|
5853
|
+
const orgId = opts.orgId ?? getRequestOrgId() ?? undefined;
|
|
5854
|
+
await notifyActionChange({
|
|
5855
|
+
actionName: toolCall.name,
|
|
5856
|
+
...(owner ? { owner } : {}),
|
|
5857
|
+
...(orgId ? { orgId } : {}),
|
|
5858
|
+
});
|
|
5859
|
+
}
|
|
5693
5860
|
} catch {
|
|
5694
5861
|
// poll module may be unavailable in non-server contexts — ignore
|
|
5695
5862
|
}
|
|
@@ -163,6 +163,46 @@ export const UNCLAIMED_BACKGROUND_RUN_REDISPATCH_BOUND_MS = 5 * 60_000;
|
|
|
163
163
|
*/
|
|
164
164
|
export const UNCLAIMED_BACKGROUND_RUN_FAST_SWEEP_MS = 20_000;
|
|
165
165
|
|
|
166
|
+
/**
|
|
167
|
+
* How long a negative `status='running'` probe stays trusted.
|
|
168
|
+
*
|
|
169
|
+
* MUST stay well below the tightest staleness window any sweep enforces
|
|
170
|
+
* (`RUN_STALE_MS`, 15s): the probe sees running rows of EVERY age, so a
|
|
171
|
+
* negative result proves there were zero running rows that instant, and a row
|
|
172
|
+
* inserted since — by this process or another one — cannot yet be old enough
|
|
173
|
+
* for a sweep to act on it. Widen this past a staleness window and a genuinely
|
|
174
|
+
* dead run becomes invisible for a whole extra tick.
|
|
175
|
+
*/
|
|
176
|
+
const NO_RUNNING_RUNS_CACHE_MS = 5_000;
|
|
177
|
+
|
|
178
|
+
let _noRunningRunsUntil = 0;
|
|
179
|
+
|
|
180
|
+
/** Test seam — the probe cache is module state, so suites must clear it. */
|
|
181
|
+
export function __resetNoRunningRunsProbeForTests(): void {
|
|
182
|
+
_noRunningRunsUntil = 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* `status = 'running'` is a strict superset of every sweep predicate in this
|
|
187
|
+
* file, so one probe answers all of them. The fast sweep runs `reapAllStaleRuns`
|
|
188
|
+
* and `listUnclaimedBackgroundRunRows` back to back every 20s; on an idle app
|
|
189
|
+
* both scan for rows that do not exist, which on a remote database is two
|
|
190
|
+
* round trips per tick per app, forever. The probe collapses that pair to one.
|
|
191
|
+
*/
|
|
192
|
+
async function hasRunningRuns(): Promise<boolean> {
|
|
193
|
+
if (Date.now() < _noRunningRunsUntil) return false;
|
|
194
|
+
const { rows } = await getDbExec().execute({
|
|
195
|
+
sql: `SELECT id FROM agent_runs WHERE status = 'running' LIMIT 1`,
|
|
196
|
+
args: [],
|
|
197
|
+
});
|
|
198
|
+
if ((rows?.length ?? 0) > 0) {
|
|
199
|
+
_noRunningRunsUntil = 0;
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
_noRunningRunsUntil = Date.now() + NO_RUNNING_RUNS_CACHE_MS;
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
166
206
|
/**
|
|
167
207
|
* FIX 3 (durable-background incident) per-turn run-count ceiling for
|
|
168
208
|
* stale-run recovery — mirrors `chainServerDrivenContinuation`'s own ledger
|
|
@@ -920,6 +960,7 @@ export async function listUnclaimedBackgroundRunRows(): Promise<
|
|
|
920
960
|
UnclaimedBackgroundRunRow[]
|
|
921
961
|
> {
|
|
922
962
|
await ensureRunTables();
|
|
963
|
+
if (!(await hasRunningRuns())) return [];
|
|
923
964
|
const client = getDbExec();
|
|
924
965
|
const { rows } = await client.execute({
|
|
925
966
|
// CAST keeps the ms-epoch param 64-bit on Postgres (see
|
|
@@ -2538,6 +2579,7 @@ export async function getCurrentTurnEventsForThread(
|
|
|
2538
2579
|
*/
|
|
2539
2580
|
export async function reapAllStaleRuns(): Promise<number> {
|
|
2540
2581
|
await ensureRunTables();
|
|
2582
|
+
if (!(await hasRunningRuns())) return 0;
|
|
2541
2583
|
const client = getDbExec();
|
|
2542
2584
|
const now = Date.now();
|
|
2543
2585
|
// Background-dispatched runs use the wider window; everything else 15s. The
|
|
@@ -2570,9 +2612,9 @@ export async function reapAllStaleRuns(): Promise<number> {
|
|
|
2570
2612
|
// `reapSingleStaleRun` re-applies the identical staleness clause per row
|
|
2571
2613
|
// (same as the bulk UPDATE previously did), so a row whose heartbeat
|
|
2572
2614
|
// landed between the SELECT above and this loop is still correctly
|
|
2573
|
-
// excluded.
|
|
2574
|
-
//
|
|
2575
|
-
//
|
|
2615
|
+
// excluded. The per-row round trips only ever run for rows the SELECT above
|
|
2616
|
+
// already found stale; on an idle app `hasRunningRuns` returns before any of
|
|
2617
|
+
// this, so the 20s fast sweep (agent-chat-plugin.ts) costs one probe.
|
|
2576
2618
|
let reapedCount = 0;
|
|
2577
2619
|
for (const row of stale.rows) {
|
|
2578
2620
|
const id = (row as { id?: unknown }).id;
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
defineEventHandler,
|
|
4
4
|
getRouterParam,
|
|
5
5
|
getHeader,
|
|
6
|
+
getQuery,
|
|
6
7
|
setResponseStatus,
|
|
7
8
|
type H3Event,
|
|
8
9
|
} from "h3";
|
|
@@ -11,6 +12,7 @@ import { getSession } from "../server/auth.js";
|
|
|
11
12
|
import { readBody } from "../server/h3-helpers.js";
|
|
12
13
|
import {
|
|
13
14
|
appStateGet,
|
|
15
|
+
appStateGetManyEntries,
|
|
14
16
|
appStatePut,
|
|
15
17
|
appStateDelete,
|
|
16
18
|
appStateList,
|
|
@@ -45,6 +47,49 @@ export const getState = defineEventHandler(async (event: H3Event) => {
|
|
|
45
47
|
return value ?? null;
|
|
46
48
|
});
|
|
47
49
|
|
|
50
|
+
/** Upper bound on one batched read, so a crafted URL can't fan out unbounded. */
|
|
51
|
+
export const MAX_APP_STATE_BATCH_KEYS = 100;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* `GET /_agent-native/application-state?keys=a,b,c` — read many keys for the
|
|
55
|
+
* caller's session in one round trip.
|
|
56
|
+
*
|
|
57
|
+
* `values` carries only the keys that have a stored row; every other requested
|
|
58
|
+
* key is listed in `missing`. A key whose stored value is `null` therefore
|
|
59
|
+
* appears in `values` with a `null` value and NOT in `missing`, which is what
|
|
60
|
+
* keeps "no row" distinguishable from "row holding an empty value".
|
|
61
|
+
*/
|
|
62
|
+
export const getStateMany = defineEventHandler(async (event: H3Event) => {
|
|
63
|
+
const sessionId = await getSessionId(event);
|
|
64
|
+
const raw = getQuery(event).keys;
|
|
65
|
+
const requested = (Array.isArray(raw) ? raw : [raw])
|
|
66
|
+
.flatMap((entry) => String(entry ?? "").split(","))
|
|
67
|
+
.map((key) => safeKey(key.trim()))
|
|
68
|
+
.filter((key) => key.length > 0);
|
|
69
|
+
const keys = [...new Set(requested)];
|
|
70
|
+
|
|
71
|
+
if (keys.length === 0) {
|
|
72
|
+
throw createError({
|
|
73
|
+
statusCode: 400,
|
|
74
|
+
statusMessage: "Query parameter `keys` is required",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (keys.length > MAX_APP_STATE_BATCH_KEYS) {
|
|
78
|
+
throw createError({
|
|
79
|
+
statusCode: 400,
|
|
80
|
+
statusMessage: `At most ${MAX_APP_STATE_BATCH_KEYS} keys may be read at once`,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const entries = await appStateGetManyEntries(sessionId, keys);
|
|
85
|
+
const values: Record<string, unknown> = {};
|
|
86
|
+
for (const entry of entries) values[entry.key] = entry.value;
|
|
87
|
+
return {
|
|
88
|
+
values,
|
|
89
|
+
missing: keys.filter((key) => !(key in values)),
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
|
|
48
93
|
export const putState = defineEventHandler(async (event: H3Event) => {
|
|
49
94
|
const sessionId = await getSessionId(event);
|
|
50
95
|
const key = safeKey(String(getRouterParam(event, "key")));
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export {
|
|
3
3
|
appStateGet,
|
|
4
4
|
appStateGetMany,
|
|
5
|
+
appStateGetManyEntries,
|
|
5
6
|
appStatePut,
|
|
6
7
|
appStateDelete,
|
|
7
8
|
appStateCompareAndSet,
|
|
@@ -22,6 +23,8 @@ export {
|
|
|
22
23
|
// H3 route handlers (for templates)
|
|
23
24
|
export {
|
|
24
25
|
getState,
|
|
26
|
+
getStateMany,
|
|
27
|
+
MAX_APP_STATE_BATCH_KEYS,
|
|
25
28
|
putState,
|
|
26
29
|
deleteState,
|
|
27
30
|
listComposeDrafts,
|