@agent-native/core 0.146.8 → 0.147.0
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/templates/analytics/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/corpus/templates/analytics/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/corpus/templates/analytics/app/routes/status.$slug.tsx +4 -2
- package/corpus/templates/analytics/scripts/first-party-analytics-bigquery-backfill.ts +56 -6
- package/corpus/templates/assets/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/corpus/templates/assets/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/corpus/templates/brain/.agents/skills/adding-a-feature/SKILL.md +9 -0
- package/corpus/templates/brain/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/corpus/templates/brain/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/corpus/templates/calendar/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/corpus/templates/calendar/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/corpus/templates/chat/.agents/skills/adding-a-feature/SKILL.md +9 -0
- package/corpus/templates/chat/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/corpus/templates/chat/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/corpus/templates/clips/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/corpus/templates/clips/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +16 -4
- package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +14 -5
- package/corpus/templates/clips/package.json +1 -0
- package/corpus/templates/clips/react-router.config.ts +7 -0
- package/corpus/templates/clips/server/plugins/auth.ts +6 -1
- package/corpus/templates/clips/shared/prerendered-public-paths.ts +21 -0
- package/corpus/templates/clips/shared/share-meta.ts +8 -3
- package/corpus/templates/content/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/corpus/templates/content/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/corpus/templates/crm/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/corpus/templates/crm/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/corpus/templates/design/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/corpus/templates/design/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/corpus/templates/dispatch/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/corpus/templates/dispatch/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/corpus/templates/dispatch/app/chat-first-pages.ts +747 -0
- package/corpus/templates/dispatch/app/i18n-data.ts +181 -0
- package/corpus/templates/dispatch/app/routes/settings.tsx +62 -1
- package/corpus/templates/dispatch/changelog/2026-08-08-chat-first-navigation-keeps-conversations-central-while-open.md +6 -0
- package/corpus/templates/factory/.agents/skills/adding-a-feature/SKILL.md +9 -0
- package/corpus/templates/factory/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/corpus/templates/factory/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/corpus/templates/factory/app/routes/factory.tsx +108 -76
- package/corpus/templates/factory/changelog/2026-08-08-scheduler-health-moves-to-settings.md +6 -0
- package/corpus/templates/forms/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/corpus/templates/forms/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/corpus/templates/macros/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/corpus/templates/macros/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/corpus/templates/mail/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/corpus/templates/mail/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/corpus/templates/plan/.agents/skills/adding-a-feature/SKILL.md +9 -0
- package/corpus/templates/plan/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/corpus/templates/plan/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/corpus/templates/plan/app/lib/plan-document-title.ts +3 -1
- package/corpus/templates/plan/app/root.tsx +1 -0
- package/corpus/templates/slides/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/corpus/templates/slides/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/corpus/templates/tasks/.agents/skills/adding-a-feature/SKILL.md +9 -0
- package/corpus/templates/tasks/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/corpus/templates/tasks/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/dist/agent/engine/builder-engine.d.ts +11 -1
- package/dist/agent/engine/builder-engine.js +11 -4
- package/dist/chat-threads/store.d.ts +2 -0
- package/dist/chat-threads/store.js +1 -1
- package/dist/cli/agent-plugin.d.ts +5 -2
- package/dist/cli/agent-plugin.js +17 -9
- package/dist/cli/code-agent-executor.js +25 -5
- package/dist/cli/code-agent-mcp-config.d.ts +21 -0
- package/dist/cli/code-agent-mcp-config.js +103 -0
- package/dist/client/NewWorkspaceAppFlow.js +4 -1
- package/dist/client/agent-chat/index.d.ts +4 -1
- package/dist/client/agent-chat/index.js +4 -1
- package/dist/client/agent-page/AgentTabsPage.d.ts +1 -0
- package/dist/client/agent-page/AgentTabsPage.js +4 -3
- package/dist/client/app-providers.d.ts +3 -1
- package/dist/client/app-providers.js +50 -5
- package/dist/client/chat/runtime.js +63 -0
- package/dist/client/chat-first/agents-pane.d.ts +2 -0
- package/dist/client/chat-first/agents-pane.js +6 -0
- package/dist/client/chat-first/app-pane.d.ts +2 -0
- package/dist/client/chat-first/app-pane.js +6 -0
- package/dist/client/chat-first/apps-rail.d.ts +2 -0
- package/dist/client/chat-first/apps-rail.js +103 -0
- package/dist/client/chat-first/browser-pane.d.ts +2 -0
- package/dist/client/chat-first/browser-pane.js +64 -0
- package/dist/client/chat-first/chat-history.d.ts +12 -0
- package/dist/client/chat-first/chat-history.js +10 -0
- package/dist/client/chat-first/copy.d.ts +2 -0
- package/dist/client/chat-first/copy.js +117 -0
- package/dist/client/chat-first/index.d.ts +13 -0
- package/dist/client/chat-first/index.js +12 -0
- package/dist/client/chat-first/primary-nav.d.ts +8 -0
- package/dist/client/chat-first/primary-nav.js +6 -0
- package/dist/client/chat-first/session-watch-pane.d.ts +2 -0
- package/dist/client/chat-first/session-watch-pane.js +9 -0
- package/dist/client/chat-first/surface-panel.d.ts +9 -0
- package/dist/client/chat-first/surface-panel.js +9 -0
- package/dist/client/chat-first/surface-tabs.d.ts +9 -0
- package/dist/client/chat-first/surface-tabs.js +74 -0
- package/dist/client/chat-first/types.d.ts +74 -0
- package/dist/client/chat-first/types.js +1 -0
- package/dist/client/chat-first-agent-activity.d.ts +11 -0
- package/dist/client/chat-first-agent-activity.js +51 -0
- package/dist/client/chat-first-surface-panel-toggle.d.ts +5 -0
- package/dist/client/chat-first-surface-panel-toggle.js +6 -0
- package/dist/client/chat-first.d.ts +208 -0
- package/dist/client/chat-first.js +816 -0
- package/dist/client/extensions/ExtensionViewer.js +8 -1
- package/dist/client/resources/McpIntegrationDialog.js +3 -2
- package/dist/client/resources/McpServerDetail.js +6 -11
- package/dist/client/resources/index.d.ts +1 -0
- package/dist/client/resources/index.js +1 -0
- package/dist/client/resources/use-mcp-servers.d.ts +20 -0
- package/dist/client/resources/use-mcp-servers.js +82 -46
- package/dist/client/settings/index.d.ts +1 -0
- package/dist/client/settings/index.js +1 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/jobs/background-automation-runner.js +19 -2
- package/dist/jobs/scheduler-health.d.ts +20 -0
- package/dist/jobs/scheduler-health.js +86 -1
- package/dist/jobs/scheduler.js +38 -1
- package/dist/mcp-client/routes.d.ts +3 -0
- package/dist/mcp-client/routes.js +14 -0
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/provider-api.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agents-bundle.d.ts +2 -0
- package/dist/server/agents-bundle.js +31 -2
- package/dist/server/credential-provider.d.ts +8 -2
- package/dist/server/credential-provider.js +13 -9
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/chat-first-app-creation.d.ts +19 -0
- package/dist/shared/chat-first-app-creation.js +75 -0
- package/dist/shared/document-title.d.ts +7 -0
- package/dist/shared/document-title.js +29 -0
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.js +2 -0
- package/dist/styles/chat-history-list.css +16 -0
- package/dist/templates/chat/.agents/skills/adding-a-feature/SKILL.md +9 -0
- package/dist/templates/chat/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/dist/templates/chat/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/dist/templates/default/.agents/skills/adding-a-feature/SKILL.md +9 -0
- package/dist/templates/default/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/dist/templates/default/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/dist/templates/default/react-router.config.ts +18 -0
- package/dist/templates/headless/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/dist/templates/headless/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/dist/templates/workspace-core/.agents/skills/adding-a-feature/SKILL.md +9 -0
- package/dist/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/dist/templates/workspace-core/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/dist/templates/workspace-core/.agents/skills/workspace-conventions/SKILL.md +7 -0
- package/package.json +3 -2
- package/src/templates/chat/.agents/skills/adding-a-feature/SKILL.md +9 -0
- package/src/templates/chat/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/src/templates/chat/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/src/templates/default/.agents/skills/adding-a-feature/SKILL.md +9 -0
- package/src/templates/default/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/src/templates/default/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/src/templates/default/react-router.config.ts +18 -0
- package/src/templates/headless/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/src/templates/headless/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/src/templates/workspace-core/.agents/skills/adding-a-feature/SKILL.md +9 -0
- package/src/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +18 -6
- package/src/templates/workspace-core/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
- package/src/templates/workspace-core/.agents/skills/workspace-conventions/SKILL.md +7 -0
|
@@ -7,6 +7,7 @@ import { buildExtensionHtml } from "../../extensions/html-shell.js";
|
|
|
7
7
|
import { extensionPath, isExtensionPathname } from "../../extensions/path.js";
|
|
8
8
|
import { getThemeVars } from "../../extensions/theme.js";
|
|
9
9
|
import { SESSION_REPLAY_IFRAME_ATTRIBUTE } from "../../session-replay-iframe-protocol.js";
|
|
10
|
+
import { normalizeDocumentTitle } from "../../shared/document-title.js";
|
|
10
11
|
import { sendToAgentChat } from "../agent-chat.js";
|
|
11
12
|
import { AgentToggleButton } from "../AgentPanel.js";
|
|
12
13
|
import { agentNativePath, appPath } from "../api-path.js";
|
|
@@ -68,7 +69,13 @@ function readExtensionTitleSuffix() {
|
|
|
68
69
|
return match?.[1]?.trim() || null;
|
|
69
70
|
}
|
|
70
71
|
function extensionDocumentTitle(name, suffix) {
|
|
71
|
-
|
|
72
|
+
const safeName = normalizeDocumentTitle(name, "Untitled extension");
|
|
73
|
+
const safeSuffix = suffix
|
|
74
|
+
? normalizeDocumentTitle(suffix, "Extensions")
|
|
75
|
+
: null;
|
|
76
|
+
return safeSuffix
|
|
77
|
+
? `${safeName} \u2014 ${safeSuffix}`
|
|
78
|
+
: `${safeName} \u2014 Extensions`;
|
|
72
79
|
}
|
|
73
80
|
function extensionRole(value) {
|
|
74
81
|
return value === "owner" ||
|
|
@@ -9,7 +9,7 @@ import { useT } from "../i18n.js";
|
|
|
9
9
|
import { cn } from "../utils.js";
|
|
10
10
|
import { buildMcpOAuthStartUrl, createMcpIntegrationFormDefaults, filterMcpIntegrations, getMcpIntegrationApiFallback, getDefaultMcpIntegrations, isCustomMcpIntegrationEnabled, navigateToMcpOAuthStart, resolveMcpIntegrationScope, shouldOfferMcpIntegrationOrganizationScope, shouldOfferMcpOrganizationScope, } from "./mcp-integration-catalog.js";
|
|
11
11
|
import { McpIntegrationLogo } from "./McpIntegrationLogo.js";
|
|
12
|
-
import { formatMcpServerError, getMcpUrlValidationError,
|
|
12
|
+
import { formatMcpServerError, getMcpUrlValidationError, useMcpServersApi, useMcpServers, } from "./use-mcp-servers.js";
|
|
13
13
|
function parseHeaderLines(text) {
|
|
14
14
|
const out = {};
|
|
15
15
|
for (const line of text.split(/\r?\n/)) {
|
|
@@ -64,6 +64,7 @@ export function McpIntegrationDialog({ open, onOpenChange, initialIntegrationId
|
|
|
64
64
|
const [error, setError] = useState(null);
|
|
65
65
|
const [testResult, setTestResult] = useState(null);
|
|
66
66
|
const inputRef = useRef(null);
|
|
67
|
+
const mcpApi = useMcpServersApi();
|
|
67
68
|
const mcpServersQuery = useMcpServers();
|
|
68
69
|
const defaultIntegrations = useMemo(() => integrations ?? getDefaultMcpIntegrations(), [integrations]);
|
|
69
70
|
const customIntegrationEnabled = useMemo(() => isCustomMcpIntegrationEnabled(), []);
|
|
@@ -250,7 +251,7 @@ export function McpIntegrationDialog({ open, onOpenChange, initialIntegrationId
|
|
|
250
251
|
setError(null);
|
|
251
252
|
setTestResult(null);
|
|
252
253
|
try {
|
|
253
|
-
const res = await
|
|
254
|
+
const res = await mcpApi.test(trimmedUrl, parseHeaderLines(headersText));
|
|
254
255
|
setTestResult(res.ok
|
|
255
256
|
? {
|
|
256
257
|
ok: true,
|
|
@@ -9,9 +9,10 @@ import { IconPlugConnected, IconAlertTriangle, IconLoader2, IconCheck, IconTestP
|
|
|
9
9
|
* delete and recreate if they need to change a URL or headers.
|
|
10
10
|
*/
|
|
11
11
|
import { useState } from "react";
|
|
12
|
-
import { agentNativePath } from "../api-path.js";
|
|
13
12
|
import { cn } from "../utils.js";
|
|
13
|
+
import { useMcpServersApi, } from "./use-mcp-servers.js";
|
|
14
14
|
export function McpServerDetail({ server }) {
|
|
15
|
+
const mcpApi = useMcpServersApi();
|
|
15
16
|
const [testing, setTesting] = useState(false);
|
|
16
17
|
const [testResult, setTestResult] = useState(null);
|
|
17
18
|
const headers = server.headers ? Object.keys(server.headers) : [];
|
|
@@ -19,17 +20,11 @@ export function McpServerDetail({ server }) {
|
|
|
19
20
|
setTesting(true);
|
|
20
21
|
setTestResult(null);
|
|
21
22
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// which we do have — so wire that up.
|
|
26
|
-
const res = await fetch(agentNativePath(`/_agent-native/mcp/servers/${encodeURIComponent(server.id)}/test?scope=${server.scope}`), {
|
|
27
|
-
method: "POST",
|
|
28
|
-
credentials: "include",
|
|
29
|
-
headers: { "Content-Type": "application/json" },
|
|
23
|
+
const result = await mcpApi.testExisting({
|
|
24
|
+
id: server.id,
|
|
25
|
+
scope: server.scope,
|
|
30
26
|
});
|
|
31
|
-
|
|
32
|
-
setTestResult(body.ok ? body : { ok: false, error: body.error });
|
|
27
|
+
setTestResult(result.ok ? result : { ok: false, error: result.error });
|
|
33
28
|
}
|
|
34
29
|
catch (err) {
|
|
35
30
|
setTestResult({ ok: false, error: err?.message ?? String(err) });
|
|
@@ -7,3 +7,4 @@ export * from "./use-builtin-capabilities.js";
|
|
|
7
7
|
export { DEFAULT_MCP_INTEGRATIONS, filterMcpIntegrations, findMcpIntegrationForText, getDefaultMcpIntegrations, isCustomMcpIntegrationEnabled, isMcpIntegrationCatalogAvailable, mergeDefaultMcpIntegrations, type DefaultMcpIntegration, } from "./mcp-integration-catalog.js";
|
|
8
8
|
export { McpIntegrationDialog, type McpIntegrationDialogProps, } from "./McpIntegrationDialog.js";
|
|
9
9
|
export { McpConnectionSuggestion, type McpConnectionSuggestionProps, type McpConnectionSuggestionVariant, } from "./McpConnectionSuggestion.js";
|
|
10
|
+
export { McpServersApiProvider, useMcpServersApi, useMcpServers, useCreateMcpServer, useDeleteMcpServer, useReconnectMcpServer, testMcpServerUrl, type CreateMcpServerArgs, type McpServer, type McpServerScope, type McpServersApi, type McpServersList, type ReconnectMcpServerArgs, type TestMcpUrlResult, } from "./use-mcp-servers.js";
|
|
@@ -7,3 +7,4 @@ export * from "./use-builtin-capabilities.js";
|
|
|
7
7
|
export { DEFAULT_MCP_INTEGRATIONS, filterMcpIntegrations, findMcpIntegrationForText, getDefaultMcpIntegrations, isCustomMcpIntegrationEnabled, isMcpIntegrationCatalogAvailable, mergeDefaultMcpIntegrations, } from "./mcp-integration-catalog.js";
|
|
8
8
|
export { McpIntegrationDialog, } from "./McpIntegrationDialog.js";
|
|
9
9
|
export { McpConnectionSuggestion, } from "./McpConnectionSuggestion.js";
|
|
10
|
+
export { McpServersApiProvider, useMcpServersApi, useMcpServers, useCreateMcpServer, useDeleteMcpServer, useReconnectMcpServer, testMcpServerUrl, } from "./use-mcp-servers.js";
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* endpoints so the Workspace UI can list, create, and delete them with the
|
|
8
8
|
* same keys/invalidations the old Settings panel used.
|
|
9
9
|
*/
|
|
10
|
+
import { type ReactNode } from "react";
|
|
10
11
|
export type McpServerScope = "user" | "org";
|
|
11
12
|
export interface McpServer {
|
|
12
13
|
id: string;
|
|
@@ -37,6 +38,25 @@ export interface McpServersList {
|
|
|
37
38
|
orgId: string | null;
|
|
38
39
|
role: string | null;
|
|
39
40
|
}
|
|
41
|
+
export interface McpServersApi {
|
|
42
|
+
list: () => Promise<McpServersList>;
|
|
43
|
+
create: (args: CreateMcpServerArgs) => Promise<McpServer>;
|
|
44
|
+
delete: (args: {
|
|
45
|
+
id: string;
|
|
46
|
+
scope: McpServerScope;
|
|
47
|
+
}) => Promise<void>;
|
|
48
|
+
reconnect: (args: ReconnectMcpServerArgs) => Promise<void>;
|
|
49
|
+
test: (url: string, headers?: Record<string, string>) => Promise<TestMcpUrlResult>;
|
|
50
|
+
testExisting: (args: {
|
|
51
|
+
id: string;
|
|
52
|
+
scope: McpServerScope;
|
|
53
|
+
}) => Promise<TestMcpUrlResult>;
|
|
54
|
+
}
|
|
55
|
+
export declare function McpServersApiProvider({ api, children, }: {
|
|
56
|
+
api: McpServersApi;
|
|
57
|
+
children: ReactNode;
|
|
58
|
+
}): import("react").FunctionComponentElement<import("react").ProviderProps<McpServersApi | null>>;
|
|
59
|
+
export declare function useMcpServersApi(): McpServersApi;
|
|
40
60
|
export declare function useMcpServers(): import("@tanstack/react-query").UseQueryResult<NoInfer<McpServersList>, Error>;
|
|
41
61
|
export interface CreateMcpServerArgs {
|
|
42
62
|
scope: McpServerScope;
|
|
@@ -8,54 +8,104 @@
|
|
|
8
8
|
* same keys/invalidations the old Settings panel used.
|
|
9
9
|
*/
|
|
10
10
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
11
|
+
import { createContext, createElement, useContext, } from "react";
|
|
11
12
|
import { agentNativePath } from "../api-path.js";
|
|
12
13
|
const ENDPOINT = agentNativePath("/_agent-native/mcp/servers");
|
|
13
14
|
const LIST_KEY = ["mcp-servers"];
|
|
15
|
+
const McpServersApiContext = createContext(null);
|
|
16
|
+
export function McpServersApiProvider({ api, children, }) {
|
|
17
|
+
return createElement(McpServersApiContext.Provider, { value: api }, children);
|
|
18
|
+
}
|
|
19
|
+
export function useMcpServersApi() {
|
|
20
|
+
return useContext(McpServersApiContext) ?? defaultMcpServersApi;
|
|
21
|
+
}
|
|
22
|
+
async function listMcpServers() {
|
|
23
|
+
const res = await fetch(ENDPOINT, { credentials: "include" });
|
|
24
|
+
if (!res.ok)
|
|
25
|
+
throw new Error(`Failed to load (${res.status})`);
|
|
26
|
+
return (await res.json());
|
|
27
|
+
}
|
|
28
|
+
async function createMcpServer(args) {
|
|
29
|
+
const res = await fetch(ENDPOINT, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
credentials: "include",
|
|
32
|
+
headers: { "Content-Type": "application/json" },
|
|
33
|
+
body: JSON.stringify(args),
|
|
34
|
+
});
|
|
35
|
+
const body = (await res.json().catch((error) => {
|
|
36
|
+
throw new Error("MCP create returned invalid JSON.", { cause: error });
|
|
37
|
+
}));
|
|
38
|
+
if (!res.ok || !body.ok) {
|
|
39
|
+
throw new Error(body.error || `Create failed (${res.status})`);
|
|
40
|
+
}
|
|
41
|
+
return body.server;
|
|
42
|
+
}
|
|
43
|
+
async function deleteMcpServer(args) {
|
|
44
|
+
const res = await fetch(`${ENDPOINT}/${encodeURIComponent(args.id)}?scope=${args.scope}`, {
|
|
45
|
+
method: "DELETE",
|
|
46
|
+
credentials: "include",
|
|
47
|
+
headers: { "Content-Type": "application/json" },
|
|
48
|
+
});
|
|
49
|
+
const body = (await res.json().catch((error) => {
|
|
50
|
+
throw new Error("MCP delete returned invalid JSON.", { cause: error });
|
|
51
|
+
}));
|
|
52
|
+
if (!res.ok || !body.ok) {
|
|
53
|
+
throw new Error(body.error || `Delete failed (${res.status})`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function reconnectMcpServer(args) {
|
|
57
|
+
const res = await fetch(reconnectMcpServerUrl(args), {
|
|
58
|
+
method: "POST",
|
|
59
|
+
credentials: "include",
|
|
60
|
+
});
|
|
61
|
+
const body = await readMcpMutationBody(res);
|
|
62
|
+
const error = typeof body?.error === "string" && body.error.trim()
|
|
63
|
+
? body.error.trim()
|
|
64
|
+
: undefined;
|
|
65
|
+
if (!res.ok || body?.ok !== true) {
|
|
66
|
+
throw new Error(error || `Reconnect failed (${res.status})`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function testExistingMcpServer(args) {
|
|
70
|
+
const res = await fetch(agentNativePath(`/_agent-native/mcp/servers/${encodeURIComponent(args.id)}/test?scope=${args.scope}`), {
|
|
71
|
+
method: "POST",
|
|
72
|
+
credentials: "include",
|
|
73
|
+
headers: { "Content-Type": "application/json" },
|
|
74
|
+
});
|
|
75
|
+
const body = (await res.json().catch((error) => {
|
|
76
|
+
throw new Error("MCP test returned invalid JSON.", { cause: error });
|
|
77
|
+
}));
|
|
78
|
+
return res.ok ? body : { ok: false, error: body.error };
|
|
79
|
+
}
|
|
80
|
+
const defaultMcpServersApi = {
|
|
81
|
+
list: listMcpServers,
|
|
82
|
+
create: createMcpServer,
|
|
83
|
+
delete: deleteMcpServer,
|
|
84
|
+
reconnect: reconnectMcpServer,
|
|
85
|
+
test: testMcpServerUrl,
|
|
86
|
+
testExisting: testExistingMcpServer,
|
|
87
|
+
};
|
|
14
88
|
export function useMcpServers() {
|
|
89
|
+
const api = useMcpServersApi();
|
|
15
90
|
return useQuery({
|
|
16
91
|
queryKey: LIST_KEY,
|
|
17
|
-
queryFn:
|
|
18
|
-
const res = await fetch(ENDPOINT, { credentials: "include" });
|
|
19
|
-
if (!res.ok)
|
|
20
|
-
throw new Error(`Failed to load (${res.status})`);
|
|
21
|
-
return (await res.json());
|
|
22
|
-
},
|
|
92
|
+
queryFn: api.list,
|
|
23
93
|
staleTime: 10_000,
|
|
24
94
|
});
|
|
25
95
|
}
|
|
26
96
|
export function useCreateMcpServer() {
|
|
27
97
|
const qc = useQueryClient();
|
|
98
|
+
const api = useMcpServersApi();
|
|
28
99
|
return useMutation({
|
|
29
|
-
mutationFn:
|
|
30
|
-
const res = await fetch(ENDPOINT, {
|
|
31
|
-
method: "POST",
|
|
32
|
-
credentials: "include",
|
|
33
|
-
headers: { "Content-Type": "application/json" },
|
|
34
|
-
body: JSON.stringify(args),
|
|
35
|
-
});
|
|
36
|
-
const body = (await res.json().catch(() => ({})));
|
|
37
|
-
if (!res.ok || !body.ok) {
|
|
38
|
-
throw new Error(body.error || `Create failed (${res.status})`);
|
|
39
|
-
}
|
|
40
|
-
return body.server;
|
|
41
|
-
},
|
|
100
|
+
mutationFn: api.create,
|
|
42
101
|
onSuccess: () => qc.invalidateQueries({ queryKey: LIST_KEY }),
|
|
43
102
|
});
|
|
44
103
|
}
|
|
45
104
|
export function useDeleteMcpServer() {
|
|
46
105
|
const qc = useQueryClient();
|
|
106
|
+
const api = useMcpServersApi();
|
|
47
107
|
return useMutation({
|
|
48
|
-
mutationFn:
|
|
49
|
-
const res = await fetch(`${ENDPOINT}/${encodeURIComponent(args.id)}?scope=${args.scope}`, {
|
|
50
|
-
method: "DELETE",
|
|
51
|
-
credentials: "include",
|
|
52
|
-
headers: { "Content-Type": "application/json" },
|
|
53
|
-
});
|
|
54
|
-
const body = (await res.json().catch(() => ({})));
|
|
55
|
-
if (!res.ok || !body.ok) {
|
|
56
|
-
throw new Error(body.error || `Delete failed (${res.status})`);
|
|
57
|
-
}
|
|
58
|
-
},
|
|
108
|
+
mutationFn: api.delete,
|
|
59
109
|
onSuccess: () => qc.invalidateQueries({ queryKey: LIST_KEY }),
|
|
60
110
|
});
|
|
61
111
|
}
|
|
@@ -75,23 +125,9 @@ function reconnectMcpServerUrl(args) {
|
|
|
75
125
|
}
|
|
76
126
|
export function useReconnectMcpServer() {
|
|
77
127
|
const qc = useQueryClient();
|
|
128
|
+
const api = useMcpServersApi();
|
|
78
129
|
return useMutation({
|
|
79
|
-
mutationFn:
|
|
80
|
-
const res = await fetch(reconnectMcpServerUrl(args), {
|
|
81
|
-
method: "POST",
|
|
82
|
-
credentials: "include",
|
|
83
|
-
});
|
|
84
|
-
const body = await readMcpMutationBody(res);
|
|
85
|
-
const error = typeof body?.error === "string" && body.error.trim()
|
|
86
|
-
? body.error.trim()
|
|
87
|
-
: undefined;
|
|
88
|
-
if (!res.ok) {
|
|
89
|
-
throw new Error(error || `Reconnect failed (${res.status})`);
|
|
90
|
-
}
|
|
91
|
-
if (body?.ok !== true) {
|
|
92
|
-
throw new Error(error || "Reconnect failed");
|
|
93
|
-
}
|
|
94
|
-
},
|
|
130
|
+
mutationFn: api.reconnect,
|
|
95
131
|
onSuccess: () => qc.invalidateQueries({ queryKey: LIST_KEY }),
|
|
96
132
|
});
|
|
97
133
|
}
|
|
@@ -5,5 +5,6 @@ export { AccountSettingsCard, AccountSettingsForm, type AccountSettingsCardProps
|
|
|
5
5
|
export { openBuilderConnectPopup, useBuilderConnectFlow, useBuilderStatus, withBuilderConnectTrackingParams, type BuilderConnectFlow, type BuilderConnectFlowOptions, type BuilderConnectStartOptions, type BuilderStatus, type OpenBuilderConnectPopupOptions, } from "./useBuilderStatus.js";
|
|
6
6
|
export { SecretsSection, type SecretsSectionProps } from "./SecretsSection.js";
|
|
7
7
|
export { SettingsGroup, SettingsRow, type SettingsGroupProps, type SettingsRowProps, } from "./SettingsRow.js";
|
|
8
|
+
export { SettingsSection, SettingsSurfaceProvider, useSettingsSurface, type SettingsSurface, } from "./SettingsSection.js";
|
|
8
9
|
export { SettingsLoadingRow, SettingsSkeleton, type SettingsLoadingRowProps, type SettingsSkeletonProps, } from "./SettingsSkeleton.js";
|
|
9
10
|
export { normalizeSettingsSection, settingsSectionDomId, useSettingsPanelController, type SettingsPanelController, type SettingsPanelControllerOptions, } from "./useSettingsPanelController.js";
|
|
@@ -5,5 +5,6 @@ export { AccountSettingsCard, AccountSettingsForm, } from "./AccountSettingsCard
|
|
|
5
5
|
export { openBuilderConnectPopup, useBuilderConnectFlow, useBuilderStatus, withBuilderConnectTrackingParams, } from "./useBuilderStatus.js";
|
|
6
6
|
export { SecretsSection } from "./SecretsSection.js";
|
|
7
7
|
export { SettingsGroup, SettingsRow, } from "./SettingsRow.js";
|
|
8
|
+
export { SettingsSection, SettingsSurfaceProvider, useSettingsSurface, } from "./SettingsSection.js";
|
|
8
9
|
export { SettingsLoadingRow, SettingsSkeleton, } from "./SettingsSkeleton.js";
|
|
9
10
|
export { normalizeSettingsSection, settingsSectionDomId, useSettingsPanelController, } from "./useSettingsPanelController.js";
|
|
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
|
|
|
62
62
|
error: string;
|
|
63
63
|
states?: undefined;
|
|
64
64
|
} | {
|
|
65
|
+
error?: undefined;
|
|
65
66
|
states: {
|
|
66
67
|
clientId: number;
|
|
67
68
|
state: string;
|
|
68
69
|
}[];
|
|
69
|
-
error?: undefined;
|
|
70
70
|
}>>;
|
|
71
71
|
/**
|
|
72
72
|
* GET /_agent-native/collab/:docId/users
|
|
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
|
|
|
77
77
|
error: string;
|
|
78
78
|
users?: undefined;
|
|
79
79
|
} | {
|
|
80
|
+
error?: undefined;
|
|
80
81
|
users: {
|
|
81
82
|
clientId: number;
|
|
82
83
|
lastSeen: number;
|
|
83
84
|
}[];
|
|
84
|
-
error?: undefined;
|
|
85
85
|
}>>;
|
package/dist/collab/routes.d.ts
CHANGED
|
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
|
|
|
26
26
|
* Body: { update: string (base64), requestSource?: string }
|
|
27
27
|
*/
|
|
28
28
|
export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
29
|
-
error: string;
|
|
30
29
|
ok?: undefined;
|
|
30
|
+
error: string;
|
|
31
31
|
} | {
|
|
32
32
|
error?: undefined;
|
|
33
33
|
ok: boolean;
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
|
|
14
14
|
*/
|
|
15
15
|
export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
16
|
-
error: string;
|
|
17
16
|
ok?: undefined;
|
|
17
|
+
error: string;
|
|
18
18
|
} | {
|
|
19
19
|
error?: undefined;
|
|
20
20
|
ok: boolean;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { collectFinalResponseTextFromAgentEvents } from "../a2a/response-text.js";
|
|
2
|
+
import { createBuilderEngine } from "../agent/engine/builder-engine.js";
|
|
2
3
|
import { getStoredModelForEngine, normalizeModelForEngine, resolveEngine, } from "../agent/engine/index.js";
|
|
3
4
|
import { actionsToEngineTools, filterInitialEngineTools, getOwnerActiveApiKey, } from "../agent/production-agent.js";
|
|
4
5
|
import { runAgentLoopDirectWithSoftTimeout } from "../agent/run-loop-with-resume.js";
|
|
@@ -9,6 +10,7 @@ import { resolveAutomationExecutionIdentity, } from "../automations/service.js";
|
|
|
9
10
|
import { createThread } from "../chat-threads/store.js";
|
|
10
11
|
import { queryOrgMembers } from "../org/context.js";
|
|
11
12
|
import { organizationIdFromResourceOwner, organizationResourceOwner, } from "../resources/store.js";
|
|
13
|
+
import { resolveBuilderCredentialsDetailed } from "../server/credential-provider.js";
|
|
12
14
|
import { runWithRequestContext, } from "../server/request-context.js";
|
|
13
15
|
import { attachAutomationRunThread, finishAutomationRun, startAutomationRun, } from "./run-history.js";
|
|
14
16
|
const BACKGROUND_RUN_STUCK_MS = 10 * 60_000;
|
|
@@ -236,18 +238,33 @@ async function executeBackgroundAutomation(options, deps, historyId) {
|
|
|
236
238
|
const availableTools = actionsToEngineTools(actions);
|
|
237
239
|
const tools = filterInitialEngineTools(availableTools, initialToolNames);
|
|
238
240
|
const userApiKey = await getOwnerActiveApiKey(ownerEmail);
|
|
239
|
-
const
|
|
241
|
+
const resolvedEngine = deps.engine ??
|
|
240
242
|
(await resolveEngine({
|
|
241
243
|
apiKey: userApiKey ?? deps.apiKey,
|
|
242
244
|
appId: deps.appId,
|
|
243
245
|
}));
|
|
246
|
+
// The run manager invokes its detached callback after the scheduler's
|
|
247
|
+
// setup stack has yielded. Capture the verified Builder pair while the
|
|
248
|
+
// owner/org identity is explicit, then keep it on the engine so a
|
|
249
|
+
// concurrent serverless run cannot make this call look unauthenticated.
|
|
250
|
+
const engine = !deps.engine && resolvedEngine.name === "builder"
|
|
251
|
+
? createBuilderEngine({
|
|
252
|
+
credentials: await resolveBuilderCredentialsDetailed({
|
|
253
|
+
userEmail: ownerEmail,
|
|
254
|
+
orgId,
|
|
255
|
+
}),
|
|
256
|
+
})
|
|
257
|
+
: resolvedEngine;
|
|
244
258
|
const modelCandidate = automation.meta.model ??
|
|
245
259
|
deps.model ??
|
|
246
260
|
(await getStoredModelForEngine(engine, { appId: deps.appId })) ??
|
|
247
261
|
engine.defaultModel;
|
|
248
262
|
const model = normalizeModelForEngine(engine, modelCandidate);
|
|
249
263
|
const systemPrompt = await deps.getSystemPrompt(ownerEmail);
|
|
250
|
-
const thread = await createThread(ownerEmail, {
|
|
264
|
+
const thread = await createThread(ownerEmail, {
|
|
265
|
+
title: threadTitle,
|
|
266
|
+
orgId: orgId ?? null,
|
|
267
|
+
});
|
|
251
268
|
const runId = createRunId(options.runIdPrefix);
|
|
252
269
|
await recordRunThread(historyId, thread.id, runId);
|
|
253
270
|
// Scheduled work is background work: it has no synchronous serverless
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type MigrationEntry } from "../db/migrations.js";
|
|
2
|
+
export declare const AUTOMATION_SCHEDULER_LEASE_MS: number;
|
|
3
|
+
export declare const AUTOMATION_SCHEDULER_LEASE_RENEWAL_MS = 60000;
|
|
2
4
|
/** Authoritative release-time schema for recurring scheduler health. */
|
|
3
5
|
export declare const AUTOMATION_SCHEDULER_HEALTH_MIGRATIONS: MigrationEntry[];
|
|
4
6
|
export declare function runAutomationSchedulerHealthMigrations(nitroApp: unknown): Promise<void>;
|
|
@@ -11,6 +13,24 @@ export interface AutomationSchedulerHealth {
|
|
|
11
13
|
runtime: string | null;
|
|
12
14
|
updatedAt: number | null;
|
|
13
15
|
}
|
|
16
|
+
/** Claim one recurring scheduler across all deployments of an app. */
|
|
17
|
+
export declare function acquireAutomationSchedulerLease(input?: {
|
|
18
|
+
appId?: string;
|
|
19
|
+
now?: number;
|
|
20
|
+
leaseMs?: number;
|
|
21
|
+
}): Promise<string | null>;
|
|
22
|
+
/** Renew a lease while a slow sweep is still executing. */
|
|
23
|
+
export declare function renewAutomationSchedulerLease(input: {
|
|
24
|
+
appId?: string;
|
|
25
|
+
owner: string;
|
|
26
|
+
now?: number;
|
|
27
|
+
leaseMs?: number;
|
|
28
|
+
}): Promise<boolean>;
|
|
29
|
+
/** Release only the lease owned by this invocation. */
|
|
30
|
+
export declare function releaseAutomationSchedulerLease(input: {
|
|
31
|
+
appId?: string;
|
|
32
|
+
owner: string;
|
|
33
|
+
}): Promise<void>;
|
|
14
34
|
/** Record the last scheduler tick without making health bookkeeping fatal. */
|
|
15
35
|
export declare function recordAutomationSchedulerHealth(input: {
|
|
16
36
|
appId?: string;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { getDbExec, intType, isPostgres } from "../db/client.js";
|
|
2
3
|
import { ensureColumnExists, ensureIndexExists, ensureTableExists, } from "../db/ddl-guard.js";
|
|
3
4
|
import { runMigrations } from "../db/migrations.js";
|
|
4
5
|
const TABLE = "automation_scheduler_health";
|
|
5
6
|
const DEFAULT_APP_ID = "default";
|
|
6
7
|
const MAX_ERROR_LENGTH = 500;
|
|
8
|
+
export const AUTOMATION_SCHEDULER_LEASE_MS = 10 * 60_000;
|
|
9
|
+
export const AUTOMATION_SCHEDULER_LEASE_RENEWAL_MS = 60_000;
|
|
7
10
|
/** Authoritative release-time schema for recurring scheduler health. */
|
|
8
11
|
export const AUTOMATION_SCHEDULER_HEALTH_MIGRATIONS = [
|
|
9
12
|
{
|
|
@@ -30,6 +33,14 @@ export const AUTOMATION_SCHEDULER_HEALTH_MIGRATIONS = [
|
|
|
30
33
|
CREATE INDEX IF NOT EXISTS idx_${TABLE}_updated ON ${TABLE} (updated_at)
|
|
31
34
|
`,
|
|
32
35
|
},
|
|
36
|
+
{
|
|
37
|
+
version: 2,
|
|
38
|
+
name: "automation-scheduler-health-lease",
|
|
39
|
+
sql: `
|
|
40
|
+
ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS lease_owner TEXT;
|
|
41
|
+
ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS lease_expires_at INTEGER;
|
|
42
|
+
`,
|
|
43
|
+
},
|
|
33
44
|
];
|
|
34
45
|
export async function runAutomationSchedulerHealthMigrations(nitroApp) {
|
|
35
46
|
await runMigrations(AUTOMATION_SCHEDULER_HEALTH_MIGRATIONS, {
|
|
@@ -56,7 +67,9 @@ async function ensureHealthTable() {
|
|
|
56
67
|
last_dispatched_at ${intType()},
|
|
57
68
|
last_error TEXT,
|
|
58
69
|
runtime TEXT,
|
|
59
|
-
updated_at ${intType()} NOT NULL
|
|
70
|
+
updated_at ${intType()} NOT NULL,
|
|
71
|
+
lease_owner TEXT,
|
|
72
|
+
lease_expires_at ${intType()}
|
|
60
73
|
)
|
|
61
74
|
`;
|
|
62
75
|
const indexSql = `CREATE INDEX IF NOT EXISTS idx_${TABLE}_updated ON ${TABLE} (updated_at)`;
|
|
@@ -69,6 +82,8 @@ async function ensureHealthTable() {
|
|
|
69
82
|
await ensureColumnExists(TABLE, "last_error", `ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS last_error TEXT`);
|
|
70
83
|
await ensureColumnExists(TABLE, "runtime", `ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS runtime TEXT`);
|
|
71
84
|
await ensureColumnExists(TABLE, "updated_at", `ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS updated_at ${intType()}`);
|
|
85
|
+
await ensureColumnExists(TABLE, "lease_owner", `ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS lease_owner TEXT`);
|
|
86
|
+
await ensureColumnExists(TABLE, "lease_expires_at", `ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS lease_expires_at ${intType()}`);
|
|
72
87
|
await ensureIndexExists(`idx_${TABLE}_updated`, indexSql);
|
|
73
88
|
return;
|
|
74
89
|
}
|
|
@@ -84,6 +99,8 @@ async function ensureHealthTable() {
|
|
|
84
99
|
["last_error", "TEXT"],
|
|
85
100
|
["runtime", "TEXT"],
|
|
86
101
|
["updated_at", `${intType()} NOT NULL DEFAULT 0`],
|
|
102
|
+
["lease_owner", "TEXT"],
|
|
103
|
+
["lease_expires_at", `${intType()}`],
|
|
87
104
|
]) {
|
|
88
105
|
if (columns.has(name))
|
|
89
106
|
continue;
|
|
@@ -106,6 +123,74 @@ async function ensureHealthTable() {
|
|
|
106
123
|
}
|
|
107
124
|
return initPromise;
|
|
108
125
|
}
|
|
126
|
+
function isUniqueViolation(error) {
|
|
127
|
+
const candidate = error;
|
|
128
|
+
return (candidate?.code === "23505" ||
|
|
129
|
+
/unique|duplicate|already exists/i.test(String(candidate?.message ?? error)));
|
|
130
|
+
}
|
|
131
|
+
function leaseRowId(appId) {
|
|
132
|
+
return rowIdForScope(appId, null);
|
|
133
|
+
}
|
|
134
|
+
/** Claim one recurring scheduler across all deployments of an app. */
|
|
135
|
+
export async function acquireAutomationSchedulerLease(input = {}) {
|
|
136
|
+
await ensureHealthTable();
|
|
137
|
+
const appId = normalizeAppId(input.appId);
|
|
138
|
+
const id = leaseRowId(appId);
|
|
139
|
+
const now = input.now ?? Date.now();
|
|
140
|
+
const leaseMs = Math.max(input.leaseMs ?? AUTOMATION_SCHEDULER_LEASE_MS, AUTOMATION_SCHEDULER_LEASE_RENEWAL_MS * 2);
|
|
141
|
+
const owner = randomUUID();
|
|
142
|
+
const expiresAt = now + leaseMs;
|
|
143
|
+
const client = getDbExec();
|
|
144
|
+
try {
|
|
145
|
+
await client.execute({
|
|
146
|
+
sql: `INSERT INTO ${TABLE}
|
|
147
|
+
(id, app_id, org_id, last_checked_at, last_dispatched_at, last_error, runtime, updated_at, lease_owner, lease_expires_at)
|
|
148
|
+
VALUES (?, ?, NULL, NULL, NULL, NULL, 'recurring-jobs', ?, ?, ?)`,
|
|
149
|
+
args: [id, appId, now, owner, expiresAt],
|
|
150
|
+
});
|
|
151
|
+
return owner;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (!isUniqueViolation(error))
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
const result = await client.execute({
|
|
158
|
+
sql: `UPDATE ${TABLE}
|
|
159
|
+
SET lease_owner = ?, lease_expires_at = ?, updated_at = ?
|
|
160
|
+
WHERE id = ?
|
|
161
|
+
AND (lease_owner IS NULL OR lease_expires_at IS NULL OR lease_expires_at <= ?)`,
|
|
162
|
+
args: [owner, expiresAt, now, id, now],
|
|
163
|
+
});
|
|
164
|
+
return Number(result.rowsAffected ?? 0) > 0 ? owner : null;
|
|
165
|
+
}
|
|
166
|
+
/** Renew a lease while a slow sweep is still executing. */
|
|
167
|
+
export async function renewAutomationSchedulerLease(input) {
|
|
168
|
+
await ensureHealthTable();
|
|
169
|
+
const now = input.now ?? Date.now();
|
|
170
|
+
const expiresAt = now + Math.max(input.leaseMs ?? AUTOMATION_SCHEDULER_LEASE_MS, 0);
|
|
171
|
+
const result = await getDbExec().execute({
|
|
172
|
+
sql: `UPDATE ${TABLE}
|
|
173
|
+
SET lease_expires_at = ?, updated_at = ?
|
|
174
|
+
WHERE id = ? AND lease_owner = ?`,
|
|
175
|
+
args: [
|
|
176
|
+
expiresAt,
|
|
177
|
+
now,
|
|
178
|
+
leaseRowId(normalizeAppId(input.appId)),
|
|
179
|
+
input.owner,
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
return Number(result.rowsAffected ?? 0) > 0;
|
|
183
|
+
}
|
|
184
|
+
/** Release only the lease owned by this invocation. */
|
|
185
|
+
export async function releaseAutomationSchedulerLease(input) {
|
|
186
|
+
await ensureHealthTable();
|
|
187
|
+
await getDbExec().execute({
|
|
188
|
+
sql: `UPDATE ${TABLE}
|
|
189
|
+
SET lease_owner = NULL, lease_expires_at = NULL, updated_at = ?
|
|
190
|
+
WHERE id = ? AND lease_owner = ?`,
|
|
191
|
+
args: [Date.now(), leaseRowId(normalizeAppId(input.appId)), input.owner],
|
|
192
|
+
});
|
|
193
|
+
}
|
|
109
194
|
function fromRow(row) {
|
|
110
195
|
return {
|
|
111
196
|
appId: String(row.app_id ?? DEFAULT_APP_ID),
|
package/dist/jobs/scheduler.js
CHANGED
|
@@ -3,7 +3,7 @@ import { backgroundRunCutOffReason, isBackgroundAutomationRunActive, resolveBack
|
|
|
3
3
|
import { nextOccurrence, isValidCron, describeCron, effectiveTimezone, } from "./cron.js";
|
|
4
4
|
import { buildJobResourceContent, parseJobResource, } from "./frontmatter.js";
|
|
5
5
|
import { claimAutomationRun, finishAutomationRun, getAutomationRun, listAutomationRuns, } from "./run-history.js";
|
|
6
|
-
import { recordAutomationSchedulerHealth } from "./scheduler-health.js";
|
|
6
|
+
import { acquireAutomationSchedulerLease, recordAutomationSchedulerHealth, releaseAutomationSchedulerLease, renewAutomationSchedulerLease, AUTOMATION_SCHEDULER_LEASE_RENEWAL_MS, } from "./scheduler-health.js";
|
|
7
7
|
// ─── Frontmatter parsing ────────────────────────────────────────────────────
|
|
8
8
|
export { classifyJobFrontmatter, classifyJobResource, normalizeJobMcpTools, parseJobResource, } from "./frontmatter.js";
|
|
9
9
|
export function parseJobFrontmatter(content) {
|
|
@@ -74,6 +74,43 @@ function subscribeToJobsResourceEvents() {
|
|
|
74
74
|
* one job cannot run twice while leaving other due jobs waiting behind it.
|
|
75
75
|
*/
|
|
76
76
|
export async function processRecurringJobs(deps) {
|
|
77
|
+
const leaseOwner = await acquireAutomationSchedulerLease({
|
|
78
|
+
appId: deps.appId,
|
|
79
|
+
});
|
|
80
|
+
if (!leaseOwner)
|
|
81
|
+
return;
|
|
82
|
+
const leaseRenewal = setInterval(() => {
|
|
83
|
+
void renewAutomationSchedulerLease({
|
|
84
|
+
appId: deps.appId,
|
|
85
|
+
owner: leaseOwner,
|
|
86
|
+
}).catch((error) => {
|
|
87
|
+
console.warn("[recurring-jobs] Scheduler lease renewal failed:", error instanceof Error ? error.message : error);
|
|
88
|
+
});
|
|
89
|
+
}, AUTOMATION_SCHEDULER_LEASE_RENEWAL_MS);
|
|
90
|
+
let primaryFailed = false;
|
|
91
|
+
try {
|
|
92
|
+
await processRecurringJobsWithLease(deps);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
primaryFailed = true;
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
clearInterval(leaseRenewal);
|
|
100
|
+
try {
|
|
101
|
+
await releaseAutomationSchedulerLease({
|
|
102
|
+
appId: deps.appId,
|
|
103
|
+
owner: leaseOwner,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (releaseError) {
|
|
107
|
+
console.warn("[recurring-jobs] Scheduler lease release failed:", releaseError instanceof Error ? releaseError.message : releaseError);
|
|
108
|
+
if (!primaryFailed)
|
|
109
|
+
throw releaseError;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function processRecurringJobsWithLease(deps) {
|
|
77
114
|
subscribeToJobsResourceEvents();
|
|
78
115
|
// Skip if we recently confirmed there are no job resources to run.
|
|
79
116
|
const nowMs = Date.now();
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* hot-reload the configured server set.
|
|
7
7
|
*
|
|
8
8
|
* GET /_agent-native/mcp/servers list user + org servers
|
|
9
|
+
* GET /_agent-native/mcp/servers/runtime-config
|
|
10
|
+
* reserved; returns 410 until a
|
|
11
|
+
* desktop capability broker exists
|
|
9
12
|
* POST /_agent-native/mcp/servers add a server
|
|
10
13
|
* DELETE /_agent-native/mcp/servers/:id remove a server (scope via ?scope=)
|
|
11
14
|
* POST /_agent-native/mcp/servers/:id/test dry-run connect (no persist)
|