@desplega.ai/agent-swarm 1.91.0 → 1.92.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/README.md +3 -2
- package/openapi.json +1005 -152
- package/package.json +6 -6
- package/plugin/skills/pages/SKILL.md +5 -2
- package/src/be/db.ts +662 -19
- package/src/be/memory/constants.ts +2 -1
- package/src/be/memory/providers/openai-embedding.ts +2 -5
- package/src/be/memory/providers/sqlite-store.ts +293 -76
- package/src/be/memory/types.ts +35 -0
- package/src/be/migrations/083_script_workflows.sql +51 -0
- package/src/be/migrations/084_script_run_journal_duration.sql +5 -0
- package/src/be/migrations/085_script_runs_kind.sql +9 -0
- package/src/be/migrations/086_pages_default_authed.sql +64 -0
- package/src/be/migrations/087_skill_files.sql +19 -0
- package/src/be/modelsdev-cache.json +42310 -38617
- package/src/be/scripts/typecheck.ts +49 -0
- package/src/be/seed-scripts/catalog/boot-triage.ts +221 -0
- package/src/be/seed-scripts/catalog/catalog-report.ts +457 -0
- package/src/be/seed-scripts/catalog/compound-insights.ts +310 -6
- package/src/be/seed-scripts/catalog/gh-pr-snapshot.ts +1 -1
- package/src/be/seed-scripts/catalog/memory-eval.ts +1059 -0
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +506 -0
- package/src/be/seed-scripts/catalog/schedule-health.ts +78 -2
- package/src/be/seed-scripts/catalog/task-context-gathering.ts +92 -0
- package/src/be/seed-scripts/catalog/task-failure-audit.ts +48 -1
- package/src/be/seed-scripts/catalog/tool-usage.ts +6 -3
- package/src/be/seed-scripts/index.ts +51 -5
- package/src/be/seed-skills/index.ts +3 -3
- package/src/be/skill-sync.ts +91 -7
- package/src/be/swarm-config-guard.ts +17 -0
- package/src/commands/runner.ts +49 -4
- package/src/heartbeat/templates.ts +20 -16
- package/src/http/db-query.ts +20 -5
- package/src/http/index.ts +51 -7
- package/src/http/mcp-user.ts +23 -0
- package/src/http/mcp.ts +58 -0
- package/src/http/memory.ts +58 -0
- package/src/http/pages.ts +1 -1
- package/src/http/script-runs.ts +557 -0
- package/src/http/scripts.ts +39 -2
- package/src/http/skills.ts +225 -0
- package/src/prompts/session-templates.ts +24 -4
- package/src/providers/claude-adapter.ts +107 -28
- package/src/script-workflows/executor.ts +110 -0
- package/src/script-workflows/harness.ts +73 -0
- package/src/script-workflows/label-lint.ts +51 -0
- package/src/script-workflows/limits.ts +22 -0
- package/src/script-workflows/supervisor.ts +139 -0
- package/src/script-workflows/workflow-ctx.ts +209 -0
- package/src/scripts-runtime/sdk-allowlist.ts +4 -0
- package/src/scripts-runtime/swarm-sdk.ts +13 -0
- package/src/scripts-runtime/types/stdlib.d.ts +61 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +61 -0
- package/src/server.ts +4 -0
- package/src/slack/handlers.ts +11 -4
- package/src/slack/message-text.ts +98 -0
- package/src/slack/thread-buffer.ts +5 -3
- package/src/tests/claude-adapter-binary.test.ts +271 -74
- package/src/tests/create-page-tool.test.ts +19 -2
- package/src/tests/db-query.test.ts +28 -0
- package/src/tests/error-tracker.test.ts +121 -0
- package/src/tests/harness-provider-resolution.test.ts +33 -0
- package/src/tests/heartbeat-checklist.test.ts +36 -0
- package/src/tests/mcp-tools.test.ts +6 -0
- package/src/tests/mcp-transport-gc.test.ts +58 -0
- package/src/tests/memory-health-endpoint.test.ts +78 -0
- package/src/tests/memory-store.test.ts +221 -1
- package/src/tests/pages-http.test.ts +20 -2
- package/src/tests/pages-storage.test.ts +26 -0
- package/src/tests/prompt-template-session.test.ts +34 -5
- package/src/tests/script-runs-http.test.ts +278 -0
- package/src/tests/script-workflows-label-lint.test.ts +43 -0
- package/src/tests/script-workflows-runtime-e2e.test.ts +170 -0
- package/src/tests/scripts-mcp-e2e.test.ts +102 -2
- package/src/tests/seed-scripts.test.ts +468 -3
- package/src/tests/skill-files-http.test.ts +171 -0
- package/src/tests/skill-files.test.ts +162 -0
- package/src/tests/skill-get-file-tool.test.ts +110 -0
- package/src/tests/skill-sync.test.ts +125 -6
- package/src/tests/slack-message-text.test.ts +250 -0
- package/src/tests/system-default-skills.test.ts +40 -0
- package/src/tools/create-page.ts +2 -2
- package/src/tools/db-query.ts +16 -6
- package/src/tools/script-runs.ts +123 -0
- package/src/tools/skills/index.ts +1 -0
- package/src/tools/skills/skill-get-file.ts +80 -0
- package/src/tools/slack-read.ts +12 -3
- package/src/tools/tool-config.ts +6 -2
- package/src/types.ts +72 -0
- package/src/utils/error-tracker.ts +40 -1
- package/src/utils/internal-ai/complete-structured.ts +10 -4
- package/src/workflows/executors/raw-llm.ts +76 -59
- package/templates/schedules/daily-blocker-digest/content.md +68 -54
- package/templates/schedules/daily-compounding-reflection/content.md +4 -4
- package/templates/schedules/daily-hn-briefing/content.md +5 -5
- package/templates/schedules/daily-workflow-health-audit/content.md +6 -6
- package/templates/schedules/gtm-weekly-review/content.md +9 -9
- package/templates/schedules/weekly-dependabot-triage/content.md +24 -20
- package/templates/skills/agentmail-sending/content.md +6 -7
- package/templates/skills/desloppify/content.md +8 -9
- package/templates/skills/jira-interaction/content.md +25 -33
- package/templates/skills/kapso-whatsapp/content.md +29 -30
- package/templates/skills/linear-interaction/content.md +8 -9
- package/templates/skills/pages/content.md +205 -55
- package/templates/skills/profile-corruption-escalation/content.md +44 -85
- package/templates/skills/script-workflows/config.json +14 -0
- package/templates/skills/script-workflows/content.md +68 -0
- package/templates/skills/sprite-cli/content.md +4 -5
- package/templates/skills/swarm-scripts/content.md +2 -3
- package/templates/skills/turso-interaction/content.md +14 -17
- package/templates/skills/workflow-iterate/content.md +38 -391
- package/templates/skills/x-api-interactions/content.md +4 -6
- package/templates/skills/scheduled-task-resilience/config.json +0 -14
- package/templates/skills/scheduled-task-resilience/content.md +0 -95
|
@@ -27,17 +27,19 @@ Goal: Review system status and your standing orders, take action if needed.
|
|
|
27
27
|
|
|
28
28
|
## Instructions
|
|
29
29
|
1. **Read your HEARTBEAT.md** — run \`read /workspace/HEARTBEAT.md\` to get the latest standing orders (the snapshot above may be slightly stale).
|
|
30
|
-
2.
|
|
31
|
-
3. **
|
|
30
|
+
2. **Prune tracked HEARTBEAT items FIRST.** Active Blockers + Watch Items + Open Discussion combined must stay at ≤10 items; 20 is the absolute max only when genuinely super busy. This cap does not apply to evergreen Standing Orders / Governance / Playbook-index reference sections. Before adding anything, re-check every tracked item against its lift trigger and remove anything resolved, stale, or past its trigger date. Lift incident detail to memory; never keep it inline in HEARTBEAT.md. Every new tracked item must include an explicit lift trigger + date; if it has no removal condition, do not add it. If the tracked list is already at/over cap, prune before (or instead of) adding — the cap is binding.
|
|
31
|
+
3. **Run seeded heartbeat data-gathering.** Use \`script-run\` with global script \`Heartbeat Audit\` and pass the current HEARTBEAT.md text as \`heartbeatMarkdown\`. It covers Rules #10/#13/#15/#16/#17: resolved stale PRs, pool-target risk schedules, schedule/provider failure clusters, and whether daily-blocker-digest ran today. The Slack thread-reply check (Rule #11) stays Lead-side; the script runtime has no Slack token.
|
|
32
|
+
4. Review the system status above plus the \`Heartbeat Audit\` result for anything that needs attention (stalled tasks, idle workers with available work, anomalies).
|
|
33
|
+
5. **CRITICAL — Reboot failure triage:** Failures with reason "worker session not found" or "worker session heartbeat is stale" indicate tasks that were INTERRUPTED by a server restart. These are NOT "expected auto-cleanup" — they represent work that was lost mid-execution. For each such failure:
|
|
32
34
|
- Check what the task was (via \`get-task-details\` with the task ID from the failure)
|
|
33
35
|
- If a retry task was auto-created (tagged \`reboot-retry\`), verify it is progressing
|
|
34
36
|
- If no retry exists and the work is still needed, re-create the task
|
|
35
37
|
- Do NOT dismiss these as "expected" or "auto-cleanup"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
6. Review your standing orders for any periodic checks or actions.
|
|
39
|
+
7. If something needs attention — take action now using your available tools (create tasks, post to Slack, cancel stuck tasks, etc.).
|
|
40
|
+
8. If everything looks healthy and no standing orders are actionable — complete this task with a brief "All clear" summary. You may NOT say "All clear" if reboot-related failures exist that haven't been triaged.
|
|
41
|
+
9. Do NOT create another heartbeat-checklist task — the system handles scheduling.
|
|
42
|
+
10. **Update HEARTBEAT.md only after pruning.** Keep it current, but keep tracked items capped: remove resolved items, add only dated lift-triggered items, and lift detail to memory instead of growing the file.`,
|
|
41
43
|
variables: [
|
|
42
44
|
{
|
|
43
45
|
name: "system_status",
|
|
@@ -73,22 +75,24 @@ The API server has just restarted (deployment, pod rotation, or crash). An aggre
|
|
|
73
75
|
{{heartbeat_content}}
|
|
74
76
|
|
|
75
77
|
## Instructions
|
|
76
|
-
1. **
|
|
78
|
+
1. **Prune tracked HEARTBEAT items FIRST.** Active Blockers + Watch Items + Open Discussion combined must stay at ≤10 items; 20 is the absolute max only when genuinely super busy. This cap does not apply to evergreen Standing Orders / Governance / Playbook-index reference sections. Before adding anything, re-check every tracked item against its lift trigger and remove anything resolved, stale, or past its trigger date. Lift incident detail to memory; never keep it inline in HEARTBEAT.md. Every new tracked item must include an explicit lift trigger + date; if it has no removal condition, do not add it. If the tracked list is already at/over cap, prune before (or instead of) adding — the cap is binding.
|
|
79
|
+
2. **Run seeded boot triage.** Use \`script-run\` with global script \`boot-triage\` to gather deploy-restart PR context, recent real failures, stuck offline-agent work, orphaned pending/offered tasks, and superseded tasks missing resume children in one read-only call.
|
|
80
|
+
3. **Triage reboot-interrupted work FIRST.** If the "Reboot-Interrupted Work" section above or the \`boot-triage\` result lists tasks:
|
|
77
81
|
- For each task: verify the retry is progressing via \`get-task-details\` with the retry task ID
|
|
78
82
|
- If a retry failed or is stuck, re-create the task manually
|
|
79
83
|
- If the work is no longer needed, cancel the retry task
|
|
80
84
|
- You MUST address every item — do NOT skip this section
|
|
81
|
-
|
|
85
|
+
4. **Verify supersede + resume worked end-to-end.** Worker crashes / OOMs are recovered via supersede (parent → \`superseded\`) + a fresh \`taskType=resume\` child created by the heartbeat sweep (DES-523). Sanity check:
|
|
82
86
|
- List recent \`superseded\` tasks: \`list-tasks status=superseded\` (last ~hour).
|
|
83
87
|
- For each, confirm a child task with \`taskType=resume\` and a non-terminal status exists. If a superseded task is missing its resume child, the work is silently dropped — recreate the task manually.
|
|
84
88
|
- Look for \`in_progress\` tasks older than 5 min on agents that show as offline — the sweep should have caught them. If any remain, recreate as needed.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
5. **Check orphaned tasks.** If the "Orphaned Tasks" section or \`boot-triage\` result lists pending/offered tasks assigned to offline workers, re-assign or cancel them.
|
|
90
|
+
6. Review agent status — are all expected workers online? If not, note which are missing.
|
|
91
|
+
7. Review your standing orders for any post-reboot checks.
|
|
92
|
+
8. Take action using your available tools.
|
|
93
|
+
9. Complete this task with a summary of what you found and what actions you took. Include the status of each reboot-interrupted task.
|
|
94
|
+
10. Do NOT create another boot-triage task — this is a one-off event.
|
|
95
|
+
11. **Update HEARTBEAT.md only after pruning.** If the reboot revealed a pattern worth monitoring, add it only as a dated lift-triggered tracked item and only if the cap still holds after pruning. Lift incident detail to memory instead of growing HEARTBEAT.md.`,
|
|
92
96
|
variables: [
|
|
93
97
|
{
|
|
94
98
|
name: "system_status",
|
package/src/http/db-query.ts
CHANGED
|
@@ -11,6 +11,24 @@ export interface DbQueryResult {
|
|
|
11
11
|
total: number;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export const DbQueryInputShape = {
|
|
15
|
+
sql: z.string().min(1).max(10_000).optional(),
|
|
16
|
+
query: z.string().min(1).max(10_000).optional().describe("Deprecated runtime alias for sql."),
|
|
17
|
+
params: z.array(z.any()).optional().default([]),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const DbQueryInputSchema = z
|
|
21
|
+
.object(DbQueryInputShape)
|
|
22
|
+
.refine((body) => body.sql !== undefined || body.query !== undefined, {
|
|
23
|
+
message: "Either sql or query is required",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export type DbQueryInput = z.infer<typeof DbQueryInputSchema>;
|
|
27
|
+
|
|
28
|
+
export function resolveDbQuerySql(input: Pick<DbQueryInput, "sql" | "query">): string {
|
|
29
|
+
return input.sql ?? input.query ?? "";
|
|
30
|
+
}
|
|
31
|
+
|
|
14
32
|
function stripTrailingSemicolon(sql: string): string {
|
|
15
33
|
return sql.trim().replace(/;\s*$/, "").trim();
|
|
16
34
|
}
|
|
@@ -67,10 +85,7 @@ const dbQueryRoute = route({
|
|
|
67
85
|
pattern: ["api", "db-query"],
|
|
68
86
|
summary: "Execute a read-only SQL query",
|
|
69
87
|
tags: ["Debug"],
|
|
70
|
-
body:
|
|
71
|
-
sql: z.string().min(1).max(10_000),
|
|
72
|
-
params: z.array(z.any()).optional().default([]),
|
|
73
|
-
}),
|
|
88
|
+
body: DbQueryInputSchema,
|
|
74
89
|
responses: {
|
|
75
90
|
200: {
|
|
76
91
|
description: "Query results",
|
|
@@ -100,7 +115,7 @@ export async function handleDbQuery(
|
|
|
100
115
|
if (!parsed) return true;
|
|
101
116
|
|
|
102
117
|
try {
|
|
103
|
-
const result = executeReadOnlyQuery(parsed.body
|
|
118
|
+
const result = executeReadOnlyQuery(resolveDbQuerySql(parsed.body), parsed.body.params);
|
|
104
119
|
json(res, result);
|
|
105
120
|
} catch (err: unknown) {
|
|
106
121
|
const message = err instanceof Error ? err.message : String(err);
|
package/src/http/index.ts
CHANGED
|
@@ -21,9 +21,11 @@ import {
|
|
|
21
21
|
withRemoteContext,
|
|
22
22
|
withSpanContext,
|
|
23
23
|
} from "../otel";
|
|
24
|
+
import { startScriptRunSupervisor, stopScriptRunSupervisor } from "../script-workflows/supervisor";
|
|
24
25
|
import { startSlackApp, stopSlackApp } from "../slack";
|
|
25
26
|
import { initTelemetry, telemetry } from "../telemetry";
|
|
26
27
|
import { getApiKey } from "../utils/api-key";
|
|
28
|
+
import { getMcpBaseUrl } from "../utils/constants";
|
|
27
29
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
28
30
|
import { initWorkflows } from "../workflows";
|
|
29
31
|
import { handleActiveSessions } from "./active-sessions";
|
|
@@ -41,12 +43,17 @@ import { handleHeartbeat } from "./heartbeat";
|
|
|
41
43
|
import { handleInboxState } from "./inbox-state";
|
|
42
44
|
import { handleIntegrations } from "./integrations";
|
|
43
45
|
import { handleKv } from "./kv";
|
|
44
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
closeIdleMcpTransports,
|
|
48
|
+
DEFAULT_MCP_TRANSPORT_IDLE_TIMEOUT_MS,
|
|
49
|
+
handleMcp,
|
|
50
|
+
type McpTransportActivity,
|
|
51
|
+
} from "./mcp";
|
|
45
52
|
import { handleMcpBridge } from "./mcp-bridge";
|
|
46
53
|
import { handleMcpOAuth, startMcpOAuthPendingGc, stopMcpOAuthPendingGc } from "./mcp-oauth";
|
|
47
54
|
import { handleMcpServers } from "./mcp-servers";
|
|
48
|
-
import { handleMcpUser } from "./mcp-user";
|
|
49
|
-
import { handleMemory } from "./memory";
|
|
55
|
+
import { closeIdleMcpUserTransports, handleMcpUser } from "./mcp-user";
|
|
56
|
+
import { handleMemory, startMemoryGc, stopMemoryGc } from "./memory";
|
|
50
57
|
import { handleMetrics } from "./metrics";
|
|
51
58
|
import { handlePageProxy } from "./page-proxy";
|
|
52
59
|
import { handlePages } from "./pages";
|
|
@@ -57,6 +64,7 @@ import { handlePromptTemplates } from "./prompt-templates";
|
|
|
57
64
|
import { handleRepos } from "./repos";
|
|
58
65
|
import { describeRequestRoute } from "./route-def";
|
|
59
66
|
import { handleSchedules } from "./schedules";
|
|
67
|
+
import { handleScriptRuns } from "./script-runs";
|
|
60
68
|
import { handleScripts } from "./scripts";
|
|
61
69
|
import { handleSessionData } from "./session-data";
|
|
62
70
|
import { handleSessions } from "./sessions";
|
|
@@ -96,12 +104,15 @@ const globalState = globalThis as typeof globalThis & {
|
|
|
96
104
|
__transports?: Record<string, StreamableHTTPServerTransport>;
|
|
97
105
|
__transportsUser?: Record<string, StreamableHTTPServerTransport>;
|
|
98
106
|
__sessionUsers?: Record<string, string>;
|
|
107
|
+
__transportActivity?: McpTransportActivity;
|
|
108
|
+
__transportActivityUser?: McpTransportActivity;
|
|
99
109
|
__sigintRegistered?: boolean;
|
|
100
110
|
__apiGcInterval?: ReturnType<typeof setInterval>;
|
|
101
111
|
__runId?: string;
|
|
102
112
|
};
|
|
103
113
|
|
|
104
114
|
const API_GC_INTERVAL_MS = 5 * 60 * 1000;
|
|
115
|
+
const MCP_TRANSPORT_IDLE_TIMEOUT_MS = DEFAULT_MCP_TRANSPORT_IDLE_TIMEOUT_MS;
|
|
105
116
|
|
|
106
117
|
type GcCapableGlobal = typeof globalThis & { gc?: () => void };
|
|
107
118
|
|
|
@@ -127,11 +138,25 @@ function startApiGcInterval() {
|
|
|
127
138
|
|
|
128
139
|
const gc = (globalThis as GcCapableGlobal).gc;
|
|
129
140
|
if (typeof gc !== "function") {
|
|
130
|
-
console.log("[HTTP] Explicit GC unavailable;
|
|
131
|
-
return;
|
|
141
|
+
console.log("[HTTP] Explicit GC unavailable; idle MCP transport sweeps remain enabled");
|
|
132
142
|
}
|
|
133
143
|
|
|
134
144
|
const interval = setInterval(() => {
|
|
145
|
+
const closedOwnerTransports = closeIdleMcpTransports(transports, transportActivity, {
|
|
146
|
+
idleTimeoutMs: MCP_TRANSPORT_IDLE_TIMEOUT_MS,
|
|
147
|
+
label: "MCP",
|
|
148
|
+
});
|
|
149
|
+
const closedUserTransports = closeIdleMcpUserTransports(
|
|
150
|
+
transportsUser,
|
|
151
|
+
sessionUsers,
|
|
152
|
+
transportActivityUser,
|
|
153
|
+
{ idleTimeoutMs: MCP_TRANSPORT_IDLE_TIMEOUT_MS },
|
|
154
|
+
);
|
|
155
|
+
if (closedOwnerTransports > 0 || closedUserTransports > 0) {
|
|
156
|
+
console.log(
|
|
157
|
+
`[HTTP] Closed ${closedOwnerTransports} owner MCP and ${closedUserTransports} user MCP idle transport(s)`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
135
160
|
scheduleApiGc("periodic API sweep");
|
|
136
161
|
}, API_GC_INTERVAL_MS);
|
|
137
162
|
interval.unref?.();
|
|
@@ -148,6 +173,8 @@ const transports: Record<string, StreamableHTTPServerTransport> = globalState.__
|
|
|
148
173
|
const transportsUser: Record<string, StreamableHTTPServerTransport> =
|
|
149
174
|
globalState.__transportsUser ?? {};
|
|
150
175
|
const sessionUsers: Record<string, string> = globalState.__sessionUsers ?? {};
|
|
176
|
+
const transportActivity: McpTransportActivity = globalState.__transportActivity ?? {};
|
|
177
|
+
const transportActivityUser: McpTransportActivity = globalState.__transportActivityUser ?? {};
|
|
151
178
|
|
|
152
179
|
const httpServer = createHttpServer(async (req, res) => {
|
|
153
180
|
const startTime = performance.now();
|
|
@@ -272,6 +299,7 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
272
299
|
() => handleMetrics(req, res, pathSegments, queryParams, myAgentId),
|
|
273
300
|
() => handleRepos(req, res, pathSegments, queryParams),
|
|
274
301
|
() => handleSkills(req, res, pathSegments, queryParams, myAgentId),
|
|
302
|
+
() => handleScriptRuns(req, res, pathSegments, queryParams, myAgentId),
|
|
275
303
|
() => handleScripts(req, res, pathSegments, queryParams, myAgentId),
|
|
276
304
|
() => handleMcpBridge(req, res, pathSegments, queryParams, myAgentId),
|
|
277
305
|
() => handleMcpServers(req, res, pathSegments, queryParams),
|
|
@@ -287,8 +315,8 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
287
315
|
() => handleSessions(req, res, pathSegments, queryParams),
|
|
288
316
|
() => handleInboxState(req, res, pathSegments, queryParams),
|
|
289
317
|
() => handleTaskTemplates(req, res, pathSegments, queryParams),
|
|
290
|
-
() => handleMcp(req, res, transports),
|
|
291
|
-
() => handleMcpUser(req, res, transportsUser, sessionUsers),
|
|
318
|
+
() => handleMcp(req, res, transports, transportActivity),
|
|
319
|
+
() => handleMcpUser(req, res, transportsUser, sessionUsers, transportActivityUser),
|
|
292
320
|
];
|
|
293
321
|
|
|
294
322
|
try {
|
|
@@ -330,6 +358,8 @@ globalState.__httpServer = httpServer;
|
|
|
330
358
|
globalState.__transports = transports;
|
|
331
359
|
globalState.__transportsUser = transportsUser;
|
|
332
360
|
globalState.__sessionUsers = sessionUsers;
|
|
361
|
+
globalState.__transportActivity = transportActivity;
|
|
362
|
+
globalState.__transportActivityUser = transportActivityUser;
|
|
333
363
|
|
|
334
364
|
async function shutdown() {
|
|
335
365
|
console.log("Shutting down HTTP server...");
|
|
@@ -343,6 +373,9 @@ async function shutdown() {
|
|
|
343
373
|
// Stop heartbeat triage
|
|
344
374
|
stopHeartbeat();
|
|
345
375
|
|
|
376
|
+
// Stop durable script workflow subprocesses
|
|
377
|
+
stopScriptRunSupervisor();
|
|
378
|
+
|
|
346
379
|
// Stop Slack bot
|
|
347
380
|
await stopSlackApp();
|
|
348
381
|
|
|
@@ -355,6 +388,9 @@ async function shutdown() {
|
|
|
355
388
|
// Stop MCP OAuth pending-session garbage collector
|
|
356
389
|
stopMcpOAuthPendingGc();
|
|
357
390
|
|
|
391
|
+
// Stop memory expired-row garbage collector
|
|
392
|
+
stopMemoryGc();
|
|
393
|
+
|
|
358
394
|
if (globalState.__apiGcInterval) {
|
|
359
395
|
clearInterval(globalState.__apiGcInterval);
|
|
360
396
|
delete globalState.__apiGcInterval;
|
|
@@ -365,6 +401,7 @@ async function shutdown() {
|
|
|
365
401
|
console.log(`[HTTP] Closing transport ${id}`);
|
|
366
402
|
transport.close();
|
|
367
403
|
delete transports[id];
|
|
404
|
+
delete transportActivity[id];
|
|
368
405
|
}
|
|
369
406
|
|
|
370
407
|
for (const [id, transport] of Object.entries(transportsUser)) {
|
|
@@ -372,6 +409,7 @@ async function shutdown() {
|
|
|
372
409
|
transport.close();
|
|
373
410
|
delete transportsUser[id];
|
|
374
411
|
delete sessionUsers[id];
|
|
412
|
+
delete transportActivityUser[id];
|
|
375
413
|
}
|
|
376
414
|
|
|
377
415
|
// Close all active connections forcefully
|
|
@@ -487,6 +525,9 @@ httpServer
|
|
|
487
525
|
// Initialize workflow engine (trigger subscriptions + resume listener)
|
|
488
526
|
initWorkflows();
|
|
489
527
|
|
|
528
|
+
// Reconcile durable script workflow subprocesses
|
|
529
|
+
startScriptRunSupervisor(getMcpBaseUrl());
|
|
530
|
+
|
|
490
531
|
// Start scheduler (if enabled)
|
|
491
532
|
if (hasCapability("scheduling")) {
|
|
492
533
|
const { startScheduler } = await import("../scheduler");
|
|
@@ -512,6 +553,9 @@ httpServer
|
|
|
512
553
|
|
|
513
554
|
// Start MCP OAuth pending-session garbage collector (5-min tick)
|
|
514
555
|
startMcpOAuthPendingGc();
|
|
556
|
+
|
|
557
|
+
// Start expired-memory garbage collector (1-hour tick, immediate first run)
|
|
558
|
+
startMemoryGc();
|
|
515
559
|
})
|
|
516
560
|
.on("error", (err) => {
|
|
517
561
|
console.error("HTTP Server Error:", err);
|
package/src/http/mcp-user.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
|
5
5
|
import { resolveUserByToken } from "@/be/users";
|
|
6
6
|
import { createUserServer } from "@/server-user";
|
|
7
7
|
import type { User } from "@/types";
|
|
8
|
+
import { closeIdleMcpTransports, type McpTransportActivity, markMcpTransportActivity } from "./mcp";
|
|
8
9
|
|
|
9
10
|
function unauthorized(res: ServerResponse): true {
|
|
10
11
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
@@ -32,6 +33,7 @@ export async function handleMcpUser(
|
|
|
32
33
|
res: ServerResponse,
|
|
33
34
|
transports: Record<string, StreamableHTTPServerTransport>,
|
|
34
35
|
sessionUsers: Record<string, string>,
|
|
36
|
+
sessionActivity: McpTransportActivity = {},
|
|
35
37
|
): Promise<boolean> {
|
|
36
38
|
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
|
37
39
|
|
|
@@ -57,16 +59,19 @@ export async function handleMcpUser(
|
|
|
57
59
|
|
|
58
60
|
if (sessionId && transports[sessionId]) {
|
|
59
61
|
transport = transports[sessionId];
|
|
62
|
+
markMcpTransportActivity(sessionActivity, sessionId);
|
|
60
63
|
} else if (!sessionId && isInitializeRequest(body)) {
|
|
61
64
|
transport = new StreamableHTTPServerTransport({
|
|
62
65
|
sessionIdGenerator: () => randomUUID(),
|
|
63
66
|
onsessioninitialized: (id) => {
|
|
64
67
|
transports[id] = transport;
|
|
65
68
|
sessionUsers[id] = user.id;
|
|
69
|
+
markMcpTransportActivity(sessionActivity, id);
|
|
66
70
|
},
|
|
67
71
|
onsessionclosed: (id) => {
|
|
68
72
|
delete transports[id];
|
|
69
73
|
delete sessionUsers[id];
|
|
74
|
+
delete sessionActivity[id];
|
|
70
75
|
},
|
|
71
76
|
});
|
|
72
77
|
|
|
@@ -74,6 +79,7 @@ export async function handleMcpUser(
|
|
|
74
79
|
if (transport.sessionId) {
|
|
75
80
|
delete transports[transport.sessionId];
|
|
76
81
|
delete sessionUsers[transport.sessionId];
|
|
82
|
+
delete sessionActivity[transport.sessionId];
|
|
77
83
|
}
|
|
78
84
|
};
|
|
79
85
|
|
|
@@ -92,11 +98,13 @@ export async function handleMcpUser(
|
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
await transport.handleRequest(req, res, body);
|
|
101
|
+
markMcpTransportActivity(sessionActivity, transport.sessionId);
|
|
95
102
|
return true;
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
if (req.method === "GET" || req.method === "DELETE") {
|
|
99
106
|
if (sessionId && transports[sessionId]) {
|
|
107
|
+
markMcpTransportActivity(sessionActivity, sessionId);
|
|
100
108
|
await transports[sessionId].handleRequest(req, res);
|
|
101
109
|
return true;
|
|
102
110
|
}
|
|
@@ -109,3 +117,18 @@ export async function handleMcpUser(
|
|
|
109
117
|
res.end("Method not allowed");
|
|
110
118
|
return true;
|
|
111
119
|
}
|
|
120
|
+
|
|
121
|
+
export function closeIdleMcpUserTransports(
|
|
122
|
+
transports: Record<string, StreamableHTTPServerTransport>,
|
|
123
|
+
sessionUsers: Record<string, string>,
|
|
124
|
+
sessionActivity: McpTransportActivity,
|
|
125
|
+
options: { now?: number; idleTimeoutMs?: number } = {},
|
|
126
|
+
): number {
|
|
127
|
+
return closeIdleMcpTransports(transports, sessionActivity, {
|
|
128
|
+
...options,
|
|
129
|
+
label: "user MCP",
|
|
130
|
+
onClose: (id) => {
|
|
131
|
+
delete sessionUsers[id];
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
package/src/http/mcp.ts
CHANGED
|
@@ -4,10 +4,62 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
4
4
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import { createServer } from "@/server";
|
|
6
6
|
|
|
7
|
+
export type McpTransportActivity = Record<string, number>;
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_MCP_TRANSPORT_IDLE_TIMEOUT_MS = 2 * 60 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
export function markMcpTransportActivity(
|
|
12
|
+
sessionActivity: McpTransportActivity,
|
|
13
|
+
sessionId: string | undefined,
|
|
14
|
+
now = Date.now(),
|
|
15
|
+
): void {
|
|
16
|
+
if (sessionId) {
|
|
17
|
+
sessionActivity[sessionId] = now;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function closeIdleMcpTransports(
|
|
22
|
+
transports: Record<string, StreamableHTTPServerTransport>,
|
|
23
|
+
sessionActivity: McpTransportActivity,
|
|
24
|
+
options: {
|
|
25
|
+
now?: number;
|
|
26
|
+
idleTimeoutMs?: number;
|
|
27
|
+
label?: string;
|
|
28
|
+
onClose?: (id: string) => void;
|
|
29
|
+
} = {},
|
|
30
|
+
): number {
|
|
31
|
+
const now = options.now ?? Date.now();
|
|
32
|
+
const idleTimeoutMs = options.idleTimeoutMs ?? DEFAULT_MCP_TRANSPORT_IDLE_TIMEOUT_MS;
|
|
33
|
+
let closed = 0;
|
|
34
|
+
|
|
35
|
+
for (const [id, transport] of Object.entries(transports)) {
|
|
36
|
+
const lastActivity = sessionActivity[id];
|
|
37
|
+
if (lastActivity === undefined) {
|
|
38
|
+
sessionActivity[id] = now;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (now - lastActivity < idleTimeoutMs) continue;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
transport.close();
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.warn(`[HTTP] Failed to close idle ${options.label ?? "MCP"} transport ${id}: ${err}`);
|
|
47
|
+
} finally {
|
|
48
|
+
delete transports[id];
|
|
49
|
+
delete sessionActivity[id];
|
|
50
|
+
options.onClose?.(id);
|
|
51
|
+
closed++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return closed;
|
|
56
|
+
}
|
|
57
|
+
|
|
7
58
|
export async function handleMcp(
|
|
8
59
|
req: IncomingMessage,
|
|
9
60
|
res: ServerResponse,
|
|
10
61
|
transports: Record<string, StreamableHTTPServerTransport>,
|
|
62
|
+
sessionActivity: McpTransportActivity = {},
|
|
11
63
|
): Promise<boolean> {
|
|
12
64
|
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
|
13
65
|
|
|
@@ -26,20 +78,24 @@ export async function handleMcp(
|
|
|
26
78
|
|
|
27
79
|
if (sessionId && transports[sessionId]) {
|
|
28
80
|
transport = transports[sessionId];
|
|
81
|
+
markMcpTransportActivity(sessionActivity, sessionId);
|
|
29
82
|
} else if (!sessionId && isInitializeRequest(body)) {
|
|
30
83
|
transport = new StreamableHTTPServerTransport({
|
|
31
84
|
sessionIdGenerator: () => randomUUID(),
|
|
32
85
|
onsessioninitialized: (id) => {
|
|
33
86
|
transports[id] = transport;
|
|
87
|
+
markMcpTransportActivity(sessionActivity, id);
|
|
34
88
|
},
|
|
35
89
|
onsessionclosed: (id) => {
|
|
36
90
|
delete transports[id];
|
|
91
|
+
delete sessionActivity[id];
|
|
37
92
|
},
|
|
38
93
|
});
|
|
39
94
|
|
|
40
95
|
transport.onclose = () => {
|
|
41
96
|
if (transport.sessionId) {
|
|
42
97
|
delete transports[transport.sessionId];
|
|
98
|
+
delete sessionActivity[transport.sessionId];
|
|
43
99
|
}
|
|
44
100
|
};
|
|
45
101
|
|
|
@@ -58,11 +114,13 @@ export async function handleMcp(
|
|
|
58
114
|
}
|
|
59
115
|
|
|
60
116
|
await transport.handleRequest(req, res, body);
|
|
117
|
+
markMcpTransportActivity(sessionActivity, transport.sessionId);
|
|
61
118
|
return true;
|
|
62
119
|
}
|
|
63
120
|
|
|
64
121
|
if (req.method === "GET" || req.method === "DELETE") {
|
|
65
122
|
if (sessionId && transports[sessionId]) {
|
|
123
|
+
markMcpTransportActivity(sessionActivity, sessionId);
|
|
66
124
|
await transports[sessionId].handleRequest(req, res);
|
|
67
125
|
return true;
|
|
68
126
|
}
|
package/src/http/memory.ts
CHANGED
|
@@ -115,6 +115,18 @@ const listMemory = route({
|
|
|
115
115
|
},
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
+
const memoryHealth = route({
|
|
119
|
+
method: "get",
|
|
120
|
+
path: "/api/memory/health",
|
|
121
|
+
pattern: ["api", "memory", "health"],
|
|
122
|
+
summary: "Report memory vector index health and retrieval mode",
|
|
123
|
+
tags: ["Memory"],
|
|
124
|
+
auth: { apiKey: true },
|
|
125
|
+
responses: {
|
|
126
|
+
200: { description: "Memory vector index health" },
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
118
130
|
const deleteMemoryById = route({
|
|
119
131
|
method: "delete",
|
|
120
132
|
path: "/api/memory/{id}",
|
|
@@ -498,6 +510,14 @@ export async function handleMemory(
|
|
|
498
510
|
return true;
|
|
499
511
|
}
|
|
500
512
|
|
|
513
|
+
if (memoryHealth.match(req.method, pathSegments)) {
|
|
514
|
+
const parsed = await memoryHealth.parse(req, res, pathSegments, new URLSearchParams());
|
|
515
|
+
if (!parsed) return true;
|
|
516
|
+
|
|
517
|
+
json(res, getMemoryStore().getHealth());
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
|
|
501
521
|
if (deleteMemoryById.match(req.method, pathSegments)) {
|
|
502
522
|
const parsed = await deleteMemoryById.parse(req, res, pathSegments, new URLSearchParams());
|
|
503
523
|
if (!parsed) return true;
|
|
@@ -663,3 +683,41 @@ export async function handleMemory(
|
|
|
663
683
|
|
|
664
684
|
return false;
|
|
665
685
|
}
|
|
686
|
+
|
|
687
|
+
// ─── Expired Memory GC ──────────────────────────────────────────────────────
|
|
688
|
+
|
|
689
|
+
const MEMORY_GC_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
690
|
+
let memoryGcTimer: ReturnType<typeof setInterval> | null = null;
|
|
691
|
+
|
|
692
|
+
export function startMemoryGc(intervalMs = MEMORY_GC_INTERVAL_MS): void {
|
|
693
|
+
if (memoryGcTimer) return;
|
|
694
|
+
|
|
695
|
+
// Run immediately on startup to clear any backlog
|
|
696
|
+
try {
|
|
697
|
+
const purged = getMemoryStore().purgeExpired();
|
|
698
|
+
if (purged > 0) {
|
|
699
|
+
console.log(`[memory-gc] Initial purge removed ${purged} expired memory row(s)`);
|
|
700
|
+
}
|
|
701
|
+
} catch (err) {
|
|
702
|
+
console.error("[memory-gc] Initial purge failed:", err);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
memoryGcTimer = setInterval(() => {
|
|
706
|
+
try {
|
|
707
|
+
const purged = getMemoryStore().purgeExpired();
|
|
708
|
+
if (purged > 0) {
|
|
709
|
+
console.log(`[memory-gc] Periodic purge removed ${purged} expired memory row(s)`);
|
|
710
|
+
}
|
|
711
|
+
} catch (err) {
|
|
712
|
+
console.error("[memory-gc] Periodic purge failed:", err);
|
|
713
|
+
}
|
|
714
|
+
}, intervalMs);
|
|
715
|
+
if (typeof memoryGcTimer?.unref === "function") memoryGcTimer.unref();
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export function stopMemoryGc(): void {
|
|
719
|
+
if (memoryGcTimer) {
|
|
720
|
+
clearInterval(memoryGcTimer);
|
|
721
|
+
memoryGcTimer = null;
|
|
722
|
+
}
|
|
723
|
+
}
|
package/src/http/pages.ts
CHANGED
|
@@ -58,7 +58,7 @@ const createPageRoute = route({
|
|
|
58
58
|
title: z.string().min(1),
|
|
59
59
|
description: z.string().optional(),
|
|
60
60
|
contentType: PageContentTypeSchema,
|
|
61
|
-
authMode: PageAuthModeSchema,
|
|
61
|
+
authMode: PageAuthModeSchema.default("authed"),
|
|
62
62
|
password: z.string().min(1).optional(),
|
|
63
63
|
body: z.string(),
|
|
64
64
|
needsCredentials: z.array(z.string()).optional(),
|