Unified · Split
| @@ -652,18 +652,16 @@ const stopServer = (baseUrl: string) => · 651 unmodified lines | |||
| 652 | const pid = typeof pidRecord?.pid === "number" ? pidRecord.pid : null; | 652 | const pid = typeof pidRecord?.pid === "number" ? pidRecord.pid : null; |
| 653 | 653 | ||
| 654 | if (pid === null) { | 654 | if (pid === null) { |
| 655 | yield* Effect.tryPromise({ | 655 | yield* Effect.tryPromise(() => rm(DEFAULT_SERVER_PID_FILE, { force: true })).pipe( |
| 656 | try: () => rm(DEFAULT_SERVER_PID_FILE, { force: true }), | 656 | Effect.ignore, |
| 657 | catch: () => undefined, | 657 | ); |
| 658 | }); | ||
| 659 | return false; | 658 | return false; |
| 660 | } | 659 | } |
| 661 | 660 | ||
| 662 | if (!isPidRunning(pid)) { | 661 | if (!isPidRunning(pid)) { |
| 663 | yield* Effect.tryPromise({ | 662 | yield* Effect.tryPromise(() => rm(DEFAULT_SERVER_PID_FILE, { force: true })).pipe( |
| 664 | try: () => rm(DEFAULT_SERVER_PID_FILE, { force: true }), | 663 | Effect.ignore, |
| 665 | catch: () => undefined, | 664 | ); |
| 666 | }); | ||
| 667 | return false; | 665 | return false; |
| 668 | } | 666 | } |
| 669 | 667 | ||
| @@ -674,10 +672,10 @@ const stopServer = (baseUrl: string) => · 4 unmodified lines | |||
| 674 | 672 | ||
| 675 | yield* waitForReachability(baseUrl, false).pipe( | 673 | yield* waitForReachability(baseUrl, false).pipe( |
| 676 | Effect.catchAll(() => | 674 | Effect.catchAll(() => |
| 677 | Effect.tryPromise({ | 675 | Effect.tryPromise(() => rm(DEFAULT_SERVER_PID_FILE, { force: true })).pipe( |
| 678 | try: () => rm(DEFAULT_SERVER_PID_FILE, { force: true }), | 676 | Effect.ignore, |
| 679 | catch: () => undefined, | 677 | Effect.zipRight(Effect.fail(new Error(`Timed out stopping local executor server pid ${pid}`))), |
| 680 | }).pipe(Effect.zipRight(Effect.fail(new Error(`Timed out stopping local executor server pid ${pid}`)))), | 678 | ), |
| 681 | ), | 679 | ), |
| 682 | ); | 680 | ); |
| 683 | 681 | ||
| @@ -176,7 +176,8 @@ export const LocalDistributionHarnessLive = Layer.scoped( · 175 unmodified lines | |||
| 176 | }), | 176 | }), |
| 177 | (path) => Effect.tryPromise({ | 177 | (path) => Effect.tryPromise({ |
| 178 | try: () => rm(path, { recursive: true, force: true }), | 178 | try: () => rm(path, { recursive: true, force: true }), |
| 179 | catch: () => undefined, | 179 | catch: (cause) => |
| 180 | cause instanceof Error ? cause : new Error(String(cause ?? "temp dir cleanup failed")), | ||
| 180 | }).pipe(Effect.orDie), | 181 | }).pipe(Effect.orDie), |
| 181 | ); | 182 | ); |
| 182 | 183 | ||
| @@ -647,12 +647,13 @@ describe("local-executor-server", () => { · 646 unmodified lines | |||
| 647 | }); | 647 | }); |
| 648 | 648 | ||
| 649 | const listed = yield* Effect.promise( | 649 | const listed = yield* Effect.promise( |
| 650 | () => mcp.client.listTools() as Promise<{ tools: Array<{ name: string }> }>, | 650 | () => mcp.client.listTools() as Promise<{ tools: Array<{ name: string; description?: string }> }>, |
| 651 | ); | 651 | ); |
| 652 | expect(listed.tools.map((tool) => tool.name)).toEqual(["execute"]); | 652 | expect(listed.tools.map((tool) => tool.name)).toEqual(["execute"]); |
| 653 | expect(mcp.client.getInstructions()).toContain( | 653 | expect(listed.tools[0]?.description).toContain("Workflow:"); |
| 654 | `npx add-mcp \"${server.baseUrl}/mcp\" --transport http --name \"executor\"`, | 654 | expect(listed.tools[0]?.description).toContain("tools.discover"); |
| 655 | ); | 655 | expect(listed.tools[0]?.description).toContain("tools.executor.sources.add"); |
| 656 | |||
| 656 | 657 | ||
| 657 | const executed = yield* Effect.promise( | 658 | const executed = yield* Effect.promise( |
| 658 | () => | 659 | () => |
| @@ -686,12 +687,12 @@ describe("local-executor-server", () => { · 27 unmodified lines | |||
| 686 | }); | 687 | }); |
| 687 | 688 | ||
| 688 | const listed = yield* Effect.promise( | 689 | const listed = yield* Effect.promise( |
| 689 | () => mcp.client.listTools() as Promise<{ tools: Array<{ name: string }> }>, | 690 | () => mcp.client.listTools() as Promise<{ tools: Array<{ name: string; description?: string }> }>, |
| 690 | ); | 691 | ); |
| 691 | expect(listed.tools.map((tool) => tool.name).sort()).toEqual(["execute", "resume"]); | 692 | expect(listed.tools.map((tool) => tool.name).sort()).toEqual(["execute", "resume"]); |
| 692 | expect(mcp.client.getInstructions()).toContain( | 693 | expect(listed.tools.find((tool) => tool.name === "execute")?.description).toContain("Workflow:"); |
| 693 | `npx add-mcp \"${server.baseUrl}/mcp\" --transport http --name \"executor\"`, | 694 | expect(listed.tools.find((tool) => tool.name === "execute")?.description).toContain("tools.discover"); |
| 694 | ); | 695 | |
| 695 | 696 | ||
| 696 | const executed = yield* Effect.promise( | 697 | const executed = yield* Effect.promise( |
| 697 | () => | 698 | () => |
| @@ -0,0 +1,37 @@ | |||
| 1 | import { useEffect, useMemo, useState } from "react"; | ||
| 2 | import { CodeBlock } from "./code-block"; | ||
| 3 | |||
| 4 | export function LocalMcpInstallCard(props: { | ||
| 5 | title?: string; | ||
| 6 | description?: string; | ||
| 7 | className?: string; | ||
| 8 | }) { | ||
| 9 | const [origin, setOrigin] = useState<string | null>(null); | ||
| 10 | |||
| 11 | useEffect(() => { | ||
| 12 | setOrigin(window.location.origin); | ||
| 13 | }, []); | ||
| 14 | |||
| 15 | const command = useMemo( | ||
| 16 | () => | ||
| 17 | origin | ||
| 18 | ? `npx add-mcp "${origin}/mcp" --transport http --name "executor"` | ||
| 19 | : 'npx add-mcp "<this-server>/mcp" --transport http --name "executor"', | ||
| 20 | [origin], | ||
| 21 | ); | ||
| 22 | |||
| 23 | return ( | ||
| 24 | <section className={props.className ?? "rounded-2xl border border-border bg-card/80 p-5"}> | ||
| 25 | <div className="mb-3 space-y-1"> | ||
| 26 | <h2 className="text-sm font-semibold text-foreground"> | ||
| 27 | {props.title ?? "Install local MCP"} | ||
| 28 | </h2> | ||
| 29 | <p className="text-[13px] text-muted-foreground"> | ||
| 30 | {props.description | ||
| 31 | ?? "Add this local executor server to an MCP client with one command. The URL uses the same origin as this web app."} | ||
| 32 | </p> | ||
| 33 | </div> | ||
| 34 | <CodeBlock code={command} lang="bash" className="rounded-xl border border-border bg-background/70" /> | ||
| 35 | </section> | ||
| 36 | ); | ||
| 37 | } | ||
| @@ -5,7 +5,10 @@ · 4 unmodified lines | |||
| 5 | * control-plane handler. Everything else (frontend assets, HMR) is | 5 | * control-plane handler. Everything else (frontend assets, HMR) is |
| 6 | * handled by Vite itself. | 6 | * handled by Vite itself. |
| 7 | */ | 7 | */ |
| 8 | import * as Cause from "effect/Cause"; | ||
| 8 | import * as Effect from "effect/Effect"; | 9 | import * as Effect from "effect/Effect"; |
| 10 | import * as Exit from "effect/Exit"; | ||
| 11 | import * as Runtime from "effect/Runtime"; | ||
| 9 | import * as Scope from "effect/Scope"; | 12 | import * as Scope from "effect/Scope"; |
| 10 | import { createLocalExecutorRequestHandler } from "@executor-v3/server"; | 13 | import { createLocalExecutorRequestHandler } from "@executor-v3/server"; |
| 11 | 14 | ||
| @@ -16,28 +19,53 @@ const truncateForLog = (value: string): string => · 4 unmodified lines | |||
| 16 | ? `${value.slice(0, MAX_LOGGED_ERROR_BODY_LENGTH)}... [truncated]` | 19 | ? `${value.slice(0, MAX_LOGGED_ERROR_BODY_LENGTH)}... [truncated]` |
| 17 | : value; | 20 | : value; |
| 18 | 21 | ||
| 19 | const formatErrorForLog = (error: unknown) => | 22 | /** |
| 20 | error instanceof Error | 23 | * Extract a detailed, human-readable description from an error. |
| 21 | ? { | 24 | * |
| 22 | name: error.name, | 25 | * When Effect rejects via `runPromise`, the thrown value is a |
| 23 | message: error.message, | 26 | * `FiberFailure` wrapping a `Cause`. `Cause.pretty` renders the full |
| 24 | stack: error.stack, | 27 | * failure tree (defects, interrupts, tagged errors, etc.) rather than the |
| 25 | } | 28 | * opaque fallback message ("An error has occurred"). |
| 26 | : error; | 29 | */ |
| 30 | const formatErrorForLog = (error: unknown): string => { | ||
| 31 | if (Runtime.isFiberFailure(error)) { | ||
| 32 | const cause = error[Runtime.FiberFailureCauseId]; | ||
| 33 | return Cause.pretty(cause); | ||
| 34 | } | ||
| 35 | |||
| 36 | if (error instanceof Error) { | ||
| 37 | return error.stack ?? error.message; | ||
| 38 | } | ||
| 39 | |||
| 40 | return String(error); | ||
| 41 | }; | ||
| 27 | 42 | ||
| 28 | // Create a long-lived scope that stays open for the lifetime of the process. | 43 | // Create a long-lived scope that stays open for the lifetime of the process. |
| 29 | const handlerPromise = Effect.runPromise( | 44 | const handlerPromise = (async () => { |
| 30 | Effect.gen(function* () { | 45 | const exit = await Effect.runPromiseExit( |
| 31 | const scope = yield* Scope.make(); | 46 | Effect.gen(function* () { |
| 32 | const handler = yield* createLocalExecutorRequestHandler().pipe( | 47 | const scope = yield* Scope.make(); |
| 33 | Effect.provideService(Scope.Scope, scope), | 48 | const handler = yield* createLocalExecutorRequestHandler().pipe( |
| 34 | ); | 49 | Effect.provideService(Scope.Scope, scope), |
| 35 | return handler; | 50 | ); |
| 36 | }), | 51 | return handler; |
| 37 | ).catch((error) => { | 52 | }), |
| 38 | console.error("[executor dev api] failed to initialize request handler", formatErrorForLog(error)); | 53 | ); |
| 39 | throw error; | 54 | |
| 40 | }); | 55 | if (Exit.isSuccess(exit)) { |
| 56 | return exit.value; | ||
| 57 | } | ||
| 58 | |||
| 59 | // Log the full Cause tree — this captures typed errors, defects (uncaught | ||
| 60 | // throws), and interrupts with their original stack traces. | ||
| 61 | console.error( | ||
| 62 | "[executor dev api] failed to initialize request handler\n\n" + | ||
| 63 | Cause.pretty(exit.cause), | ||
| 64 | ); | ||
| 65 | |||
| 66 | // Re-throw so every subsequent request sees the init failure. | ||
| 67 | throw Cause.squash(exit.cause); | ||
| 68 | })(); | ||
| 41 | 69 | ||
| 42 | export default { | 70 | export default { |
| 43 | async fetch(request: Request) { | 71 | async fetch(request: Request) { |
| @@ -18,6 +18,7 @@ import { · 17 unmodified lines | |||
| 18 | } from "@executor-v3/react"; | 18 | } from "@executor-v3/react"; |
| 19 | import { Badge } from "../components/ui/badge"; | 19 | import { Badge } from "../components/ui/badge"; |
| 20 | import { Button } from "../components/ui/button"; | 20 | import { Button } from "../components/ui/button"; |
| 21 | import { LocalMcpInstallCard } from "../components/local-mcp-install-card"; | ||
| 21 | import { SourceFavicon } from "../components/source-favicon"; | 22 | import { SourceFavicon } from "../components/source-favicon"; |
| 22 | import { | 23 | import { |
| 23 | IconArrowLeft, | 24 | IconArrowLeft, |
| @@ -737,6 +738,14 @@ export function AddSourcePage() { · 713 unmodified lines | |||
| 737 | 738 | ||
| 738 | {statusBanner && <StatusBanner state={statusBanner} className="mb-6" />} | 739 | {statusBanner && <StatusBanner state={statusBanner} className="mb-6" />} |
| 739 | 740 | ||
| 741 | |||
| 742 | {(phase === "idle" || phase === "discovering") && ( | ||
| 743 | <LocalMcpInstallCard | ||
| 744 | className="mb-6 rounded-xl border border-border bg-card/80 p-5" | ||
| 745 | title="Install this executor as MCP" | ||
| 746 | description="Prefer a one-command setup? Install this local executor server into your MCP client, or add an external MCP source below." | ||
| 747 | /> | ||
| 748 | )} | ||
| 740 | {/* Step 1: Discovery */} | 749 | {/* Step 1: Discovery */} |
| 741 | {(phase === "idle" || phase === "discovering") && ( | 750 | {(phase === "idle" || phase === "discovering") && ( |
| 742 | <div className="space-y-6"> | 751 | <div className="space-y-6"> |
| @@ -1,6 +1,7 @@ | |||
| 1 | import { Link } from "@tanstack/react-router"; | 1 | import { Link } from "@tanstack/react-router"; |
| 2 | import { useSources, type Source } from "@executor-v3/react"; | 2 | import { useSources, type Source } from "@executor-v3/react"; |
| 3 | import { LoadableBlock } from "../components/loadable"; | 3 | import { LoadableBlock } from "../components/loadable"; |
| 4 | import { LocalMcpInstallCard } from "../components/local-mcp-install-card"; | ||
| 4 | import { SourceFavicon } from "../components/source-favicon"; | 5 | import { SourceFavicon } from "../components/source-favicon"; |
| 5 | import { Badge } from "../components/ui/badge"; | 6 | import { Badge } from "../components/ui/badge"; |
| 6 | import { Button } from "../components/ui/button"; | 7 | import { Button } from "../components/ui/button"; |
| @@ -30,6 +31,8 @@ export function HomePage() { · 23 unmodified lines | |||
| 30 | </Link> | 31 | </Link> |
| 31 | </div> | 32 | </div> |
| 32 | 33 | ||
| 34 | <LocalMcpInstallCard className="mb-8" /> | ||
| 35 | |||
| 33 | {/* Source list */} | 36 | {/* Source list */} |
| 34 | <LoadableBlock loadable={sources} loading="Loading sources..."> | 37 | <LoadableBlock loadable={sources} loading="Loading sources..."> |
| 35 | {(items) => | 38 | {(items) => |
| @@ -11,8 +11,8 @@ export default defineConfig({ · 10 unmodified lines | |||
| 11 | devServer({ | 11 | devServer({ |
| 12 | entry: "src/dev.ts", | 12 | entry: "src/dev.ts", |
| 13 | exclude: [ | 13 | exclude: [ |
| 14 | // Only let /v1 requests reach the API handler | 14 | // Only let /v1 and /mcp requests reach the API handler |
| 15 | /^\/(?!v1(\/|$))/, | 15 | /^\/(?!(v1|mcp)(\/|$))/, |
| 16 | /^\/(src|node_modules|@vite|@id|@react-refresh)/, | 16 | /^\/(src|node_modules|@vite|@id|@react-refresh)/, |
| 17 | /\.(css|ts|tsx|js|jsx|svg|png|jpg|gif|ico|woff2?|json|map)(\?.*)?$/, | 17 | /\.(css|ts|tsx|js|jsx|svg|png|jpg|gif|ico|woff2?|json|map)(\?.*)?$/, |
| 18 | ], | 18 | ], |
| @@ -2,6 +2,8 @@ import { ElicitRequestSchema } from "@modelcontextprotocol/sdk/types.js"; · 1 unmodified line | |||
| 2 | import * as Data from "effect/Data"; | 2 | import * as Data from "effect/Data"; |
| 3 | import * as Either from "effect/Either"; | 3 | import * as Either from "effect/Either"; |
| 4 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as Cause from "effect/Cause"; | ||
| 6 | import * as Exit from "effect/Exit"; | ||
| 5 | import * as PartitionedSemaphore from "effect/PartitionedSemaphore"; | 7 | import * as PartitionedSemaphore from "effect/PartitionedSemaphore"; |
| 6 | 8 | ||
| 7 | import { | 9 | import { |
| @@ -88,10 +90,11 @@ const inputSchemaFromManifest = (inputSchemaJson: string | undefined) => { · 80 unmodified lines | |||
| 88 | const closeConnection = (connection: McpConnection): Effect.Effect<void> => | 90 | const closeConnection = (connection: McpConnection): Effect.Effect<void> => |
| 89 | Effect.tryPromise({ | 91 | Effect.tryPromise({ |
| 90 | try: () => connection.close?.() ?? Promise.resolve(), | 92 | try: () => connection.close?.() ?? Promise.resolve(), |
| 91 | catch: () => undefined, | 93 | catch: (cause) => |
| 94 | cause instanceof Error ? cause : new Error(String(cause ?? "mcp connection close failed")), | ||
| 92 | }).pipe( | 95 | }).pipe( |
| 93 | Effect.asVoid, | 96 | Effect.asVoid, |
| 94 | Effect.catchAll(() => Effect.succeed(undefined)), | 97 | Effect.catchAll(() => Effect.void), |
| 95 | ); | 98 | ); |
| 96 | 99 | ||
| 97 | const withConnectionEffect = <A, E>(input: { | 100 | const withConnectionEffect = <A, E>(input: { |
| @@ -192,7 +195,13 @@ const installMcpElicitationHandler = (input: { · 94 unmodified lines | |||
| 192 | }), | 195 | }), |
| 193 | ), | 196 | ), |
| 194 | Effect.map(toMcpElicitationResponse), | 197 | Effect.map(toMcpElicitationResponse), |
| 195 | Effect.catchAll(() => Effect.succeed({ action: "cancel" as const })), | 198 | Effect.catchAll((error) => { |
| 199 | console.error( | ||
| 200 | `[mcp-tools] elicitation failed for ${input.toolName}, treating as cancel:`, | ||
| 201 | error instanceof Error ? error.message : String(error), | ||
| 202 | ); | ||
| 203 | return Effect.succeed({ action: "cancel" as const }); | ||
| 204 | }), | ||
| 196 | ), | 205 | ), |
| 197 | ); | 206 | ); |
| 198 | }); | 207 | }); |
| @@ -427,8 +436,8 @@ export const createMcpToolsFromManifest = (input: { · 228 unmodified lines | |||
| 427 | tool: { | 436 | tool: { |
| 428 | description: entry.description ?? `MCP tool: ${entry.toolName}`, | 437 | description: entry.description ?? `MCP tool: ${entry.toolName}`, |
| 429 | inputSchema: inputSchemaFromManifest(entry.inputSchemaJson), | 438 | inputSchema: inputSchemaFromManifest(entry.inputSchemaJson), |
| 430 | execute: (args: unknown, executionContext?: ToolExecutionContext) => | 439 | execute: async (args: unknown, executionContext?: ToolExecutionContext) => { |
| 431 | Effect.runPromise( | 440 | const exit = await Effect.runPromiseExit( |
| 432 | withConnectionEffect({ | 441 | withConnectionEffect({ |
| 433 | connect: input.connect, | 442 | connect: input.connect, |
| 434 | onConnectError: (cause) => | 443 | onConnectError: (cause) => |
| @@ -453,7 +462,10 @@ export const createMcpToolsFromManifest = (input: { · 18 unmodified lines | |||
| 453 | : callEffect; | 462 | : callEffect; |
| 454 | }, | 463 | }, |
| 455 | }), | 464 | }), |
| 456 | ), | 465 | ); |
| 466 | if (Exit.isSuccess(exit)) return exit.value; | ||
| 467 | throw Cause.squash(exit.cause); | ||
| 468 | }, | ||
| 457 | }, | 469 | }, |
| 458 | metadata: { | 470 | metadata: { |
| 459 | sourceKey, | 471 | sourceKey, |
| @@ -113,7 +113,8 @@ const runMigrationsEffect = ( · 112 unmodified lines | |||
| 113 | const closeRuntimeEffect = (runtime: SqlRuntime) => | 113 | const closeRuntimeEffect = (runtime: SqlRuntime) => |
| 114 | Effect.tryPromise({ | 114 | Effect.tryPromise({ |
| 115 | try: () => runtime.close(), | 115 | try: () => runtime.close(), |
| 116 | catch: () => undefined, | 116 | catch: (cause) => |
| 117 | cause instanceof Error ? cause : new Error(String(cause ?? "unknown close error")), | ||
| 117 | }).pipe(Effect.orDie); | 118 | }).pipe(Effect.orDie); |
| 118 | 119 | ||
| 119 | export const createSqlControlPlanePersistence = ( | 120 | export const createSqlControlPlanePersistence = ( |
| @@ -150,7 +151,8 @@ export const SqlControlPlanePersistenceLive = ( · 30 unmodified lines | |||
| 150 | (persistence) => | 151 | (persistence) => |
| 151 | Effect.tryPromise({ | 152 | Effect.tryPromise({ |
| 152 | try: () => persistence.close(), | 153 | try: () => persistence.close(), |
| 153 | catch: () => undefined, | 154 | catch: (cause) => |
| 155 | cause instanceof Error ? cause : new Error(String(cause ?? "unknown close error")), | ||
| 154 | }).pipe(Effect.orDie), | 156 | }).pipe(Effect.orDie), |
| 155 | ), | 157 | ), |
| 156 | ); | 158 | ); |
| @@ -3,8 +3,8 @@ import { drizzle as drizzlePGlite } from "drizzle-orm/pglite"; · 2 unmodified lines | |||
| 3 | import { migrate as migratePGlite } from "drizzle-orm/pglite/migrator"; | 3 | import { migrate as migratePGlite } from "drizzle-orm/pglite/migrator"; |
| 4 | import { drizzle as drizzlePostgres } from "drizzle-orm/postgres-js"; | 4 | import { drizzle as drizzlePostgres } from "drizzle-orm/postgres-js"; |
| 5 | import { migrate as migratePostgres } from "drizzle-orm/postgres-js/migrator"; | 5 | import { migrate as migratePostgres } from "drizzle-orm/postgres-js/migrator"; |
| 6 | import { existsSync, readdirSync } from "node:fs"; | 6 | import { existsSync, readdirSync, readFileSync } from "node:fs"; |
| 7 | import { mkdir } from "node:fs/promises"; | 7 | import { mkdir, unlink } from "node:fs/promises"; |
| 8 | import * as path from "node:path"; | 8 | import * as path from "node:path"; |
| 9 | import { fileURLToPath } from "node:url"; | 9 | import { fileURLToPath } from "node:url"; |
| 10 | import postgres from "postgres"; | 10 | import postgres from "postgres"; |
| @@ -44,6 +44,46 @@ const trim = (value: string | undefined): string | undefined => { · 33 unmodified lines | |||
| 44 | const isPostgresUrl = (value: string): boolean => | 44 | const isPostgresUrl = (value: string): boolean => |
| 45 | value.startsWith("postgres://") || value.startsWith("postgresql://"); | 45 | value.startsWith("postgres://") || value.startsWith("postgresql://"); |
| 46 | 46 | ||
| 47 | /** | ||
| 48 | * Remove a stale PGlite `postmaster.pid` lock file if present. | ||
| 49 | * | ||
| 50 | * PGlite writes this file on open and removes it on close. When a process | ||
| 51 | * exits without calling `PGlite.close()` (e.g. the Vite dev server is | ||
| 52 | * killed with SIGKILL, or the terminal is closed), the lock file is left | ||
| 53 | * behind and prevents subsequent PGlite instances from opening the | ||
| 54 | * database. PGlite uses a synthetic PID (e.g. `-42`) that is never a real | ||
| 55 | * OS process, so we cannot use it to detect a live owner. Instead we rely | ||
| 56 | * on the fact that in the local single-user context only one process | ||
| 57 | * should ever own this database at a time. | ||
| 58 | */ | ||
| 59 | const cleanupStalePGliteLock = async (dataDir: string): Promise<void> => { | ||
| 60 | const lockPath = path.join(dataDir, "postmaster.pid"); | ||
| 61 | if (!existsSync(lockPath)) { | ||
| 62 | return; | ||
| 63 | } | ||
| 64 | |||
| 65 | // PGlite's synthetic PID is always negative (e.g. -42). A real Postgres | ||
| 66 | // postmaster would write a positive PID. If we ever see a positive PID | ||
| 67 | // we leave the file alone. | ||
| 68 | try { | ||
| 69 | const content = readFileSync(lockPath, "utf-8"); | ||
| 70 | const firstLine = content.split("\n")[0]?.trim(); | ||
| 71 | const pid = Number(firstLine); | ||
| 72 | if (!Number.isNaN(pid) && pid > 0) { | ||
| 73 | // Looks like a real Postgres PID — don't touch it. | ||
| 74 | return; | ||
| 75 | } | ||
| 76 | } catch { | ||
| 77 | // If we can't read the file, try to remove it anyway. | ||
| 78 | } | ||
| 79 | |||
| 80 | try { | ||
| 81 | await unlink(lockPath); | ||
| 82 | } catch { | ||
| 83 | // Best-effort cleanup; PGlite will report the real error if this fails. | ||
| 84 | } | ||
| 85 | }; | ||
| 86 | |||
| 47 | const createPGliteRuntime = async (localDataDir: string): Promise<SqlRuntime> => { | 87 | const createPGliteRuntime = async (localDataDir: string): Promise<SqlRuntime> => { |
| 48 | const normalized = trim(localDataDir) ?? ".executor-v3/control-plane-pgdata"; | 88 | const normalized = trim(localDataDir) ?? ".executor-v3/control-plane-pgdata"; |
| 49 | 89 | ||
| @@ -55,6 +95,7 @@ const createPGliteRuntime = async (localDataDir: string): Promise<SqlRuntime> => · 5 unmodified lines | |||
| 55 | if (!existsSync(resolvedDataDir)) { | 95 | if (!existsSync(resolvedDataDir)) { |
| 56 | await mkdir(resolvedDataDir, { recursive: true }); | 96 | await mkdir(resolvedDataDir, { recursive: true }); |
| 57 | } | 97 | } |
| 98 | await cleanupStalePGliteLock(resolvedDataDir); | ||
| 58 | client = new PGlite(resolvedDataDir); | 99 | client = new PGlite(resolvedDataDir); |
| 59 | } | 100 | } |
| 60 | 101 | ||
| @@ -15,8 +15,17 @@ import { · 14 unmodified lines | |||
| 15 | type WorkspaceId, | 15 | type WorkspaceId, |
| 16 | } from "#schema"; | 16 | } from "#schema"; |
| 17 | import * as Effect from "effect/Effect"; | 17 | import * as Effect from "effect/Effect"; |
| 18 | import * as Cause from "effect/Cause"; | ||
| 19 | import * as Exit from "effect/Exit"; | ||
| 18 | import * as Schema from "effect/Schema"; | 20 | import * as Schema from "effect/Schema"; |
| 19 | 21 | ||
| 22 | /** Run an Effect as a Promise, preserving the original error (not FiberFailure). */ | ||
| 23 | const runEffect = async <A>(effect: Effect.Effect<A, unknown, never>): Promise<A> => { | ||
| 24 | const exit = await Effect.runPromiseExit(effect); | ||
| 25 | if (Exit.isSuccess(exit)) return exit.value; | ||
| 26 | throw Cause.squash(exit.cause); | ||
| 27 | }; | ||
| 28 | |||
| 20 | import { | 29 | import { |
| 21 | type ExecutorAddSourceInput, | 30 | type ExecutorAddSourceInput, |
| 22 | type ExecutorHttpSourceAuthInput, | 31 | type ExecutorHttpSourceAuthInput, |
| @@ -277,7 +286,7 @@ export const createExecutorToolMap = (input: { · 254 unmodified lines | |||
| 277 | executionId, | 286 | executionId, |
| 278 | interactionId, | 287 | interactionId, |
| 279 | }; | 288 | }; |
| 280 | const result = await Effect.runPromise( | 289 | const result = await runEffect( |
| 281 | input.sourceAuthService.addExecutorSource( | 290 | input.sourceAuthService.addExecutorSource( |
| 282 | preparedArgs, | 291 | preparedArgs, |
| 283 | context?.onElicitation | 292 | context?.onElicitation |
| @@ -304,7 +313,7 @@ export const createExecutorToolMap = (input: { · 20 unmodified lines | |||
| 304 | ExecutorAddSourceInput, | 313 | ExecutorAddSourceInput, |
| 305 | { kind: "openapi" | "graphql" } | 314 | { kind: "openapi" | "graphql" } |
| 306 | >; | 315 | >; |
| 307 | const selectedAuth = await Effect.runPromise( | 316 | const selectedAuth = await runEffect( |
| 308 | promptForSourceCredentialSelection({ | 317 | promptForSourceCredentialSelection({ |
| 309 | args: { | 318 | args: { |
| 310 | ...preparedHttpArgs, | 319 | ...preparedHttpArgs, |
| @@ -323,7 +332,7 @@ export const createExecutorToolMap = (input: { · 12 unmodified lines | |||
| 323 | }), | 332 | }), |
| 324 | ); | 333 | ); |
| 325 | 334 | ||
| 326 | const completed = await Effect.runPromise( | 335 | const completed = await runEffect( |
| 327 | input.sourceAuthService.addExecutorSource( | 336 | input.sourceAuthService.addExecutorSource( |
| 328 | { | 337 | { |
| 329 | ...preparedHttpArgs, | 338 | ...preparedHttpArgs, |
| @@ -355,7 +364,7 @@ export const createExecutorToolMap = (input: { · 25 unmodified lines | |||
| 355 | throw new Error("executor.sources.add requires an elicitation-capable host"); | 364 | throw new Error("executor.sources.add requires an elicitation-capable host"); |
| 356 | } | 365 | } |
| 357 | 366 | ||
| 358 | const response: ElicitationResponse = await Effect.runPromise( | 367 | const response: ElicitationResponse = await runEffect( |
| 359 | context.onElicitation({ | 368 | context.onElicitation({ |
| 360 | interactionId, | 369 | interactionId, |
| 361 | path: context.path ?? asToolPath("executor.sources.add"), | 370 | path: context.path ?? asToolPath("executor.sources.add"), |
| @@ -376,7 +385,7 @@ export const createExecutorToolMap = (input: { · 14 unmodified lines | |||
| 376 | throw new Error(`Source add was not completed for ${result.source.id}`); | 385 | throw new Error(`Source add was not completed for ${result.source.id}`); |
| 377 | } | 386 | } |
| 378 | 387 | ||
| 379 | return await Effect.runPromise( | 388 | return await runEffect( |
| 380 | input.sourceAuthService.getSourceById({ | 389 | input.sourceAuthService.getSourceById({ |
| 381 | workspaceId: input.workspaceId, | 390 | workspaceId: input.workspaceId, |
| 382 | sourceId: result.source.id, | 391 | sourceId: result.source.id, |
| @@ -192,6 +192,6 @@ export const createSqlControlPlaneRuntime = ( · 191 unmodified lines | |||
| 192 | persistence, | 192 | persistence, |
| 193 | localInstallation, | 193 | localInstallation, |
| 194 | runtimeLayer: concreteRuntimeLayer, | 194 | runtimeLayer: concreteRuntimeLayer, |
| 195 | close: () => Effect.runPromise(Scope.close(scope, Exit.void)), | 195 | close: () => Effect.runPromise(Scope.close(scope, Exit.void)).catch(() => {}), |
| 196 | }; | 196 | }; |
| 197 | }); | 197 | }); |
| @@ -4,11 +4,16 @@ import type { · 3 unmodified lines | |||
| 4 | SqlControlPlaneRuntime, | 4 | SqlControlPlaneRuntime, |
| 5 | } from "@executor-v3/control-plane"; | 5 | } from "@executor-v3/control-plane"; |
| 6 | import { | 6 | import { |
| 7 | EXECUTOR_SOURCES_ADD_HELP_LINES, | ||
| 8 | ExecutionIdSchema, | ||
| 9 | RuntimeExecutionResolverService, | ||
| 7 | createExecution, | 10 | createExecution, |
| 8 | getExecution, | 11 | getExecution, |
| 9 | resumeExecution, | 12 | resumeExecution, |
| 10 | } from "@executor-v3/control-plane"; | 13 | } from "@executor-v3/control-plane"; |
| 11 | import * as Effect from "effect/Effect"; | 14 | import * as Effect from "effect/Effect"; |
| 15 | import * as Cause from "effect/Cause"; | ||
| 16 | import * as Exit from "effect/Exit"; | ||
| 12 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | 17 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; |
| 13 | import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; | 18 | import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; |
| 14 | import { z } from "zod/v4"; | 19 | import { z } from "zod/v4"; |
| @@ -110,14 +115,27 @@ const parseInteractionPayload = ( · 95 unmodified lines | |||
| 110 | } | 115 | } |
| 111 | }; | 116 | }; |
| 112 | 117 | ||
| 113 | const runControlPlane = <A, E, R>( | 118 | const runControlPlane = async <A, E, R>( |
| 114 | runtime: SqlControlPlaneRuntime, | 119 | runtime: SqlControlPlaneRuntime, |
| 115 | effect: Effect.Effect<A, E, R>, | 120 | effect: Effect.Effect<A, E, R>, |
| 116 | ): Promise<A> => | 121 | ): Promise<A> => { |
| 117 | Effect.runPromise( | 122 | const exit = await Effect.runPromiseExit( |
| 118 | effect.pipe(Effect.provide(runtime.runtimeLayer)) as Effect.Effect<A, E, never>, | 123 | effect.pipe(Effect.provide(runtime.runtimeLayer)) as Effect.Effect<A, E, never>, |
| 119 | ); | 124 | ); |
| 120 | 125 | ||
| 126 | if (Exit.isSuccess(exit)) { | ||
| 127 | return exit.value; | ||
| 128 | } | ||
| 129 | |||
| 130 | const error = Cause.squash(exit.cause); | ||
| 131 | if (error instanceof Error) { | ||
| 132 | // Preserve the original error with its stack trace and message rather | ||
| 133 | // than wrapping it in an opaque FiberFailure. | ||
| 134 | throw error; | ||
| 135 | } | ||
| 136 | throw error; | ||
| 137 | }; | ||
| 138 | |||
| 121 | const sleep = (ms: number) => | 139 | const sleep = (ms: number) => |
| 122 | new Promise<void>((resolve) => { | 140 | new Promise<void>((resolve) => { |
| 123 | setTimeout(resolve, ms); | 141 | setTimeout(resolve, ms); |
| @@ -128,6 +146,63 @@ const supportsManagedElicitation = (server: McpServer): boolean => { · 4 unmodified lines | |||
| 128 | return Boolean(capabilities?.elicitation?.form) && Boolean(capabilities?.elicitation?.url); | 146 | return Boolean(capabilities?.elicitation?.form) && Boolean(capabilities?.elicitation?.url); |
| 129 | }; | 147 | }; |
| 130 | 148 | ||
| 149 | type CatalogLike = { | ||
| 150 | listNamespaces: (input: { limit: number }) => Effect.Effect< | ||
| 151 | ReadonlyArray<{ namespace: string; displayName?: string }>, | ||
| 152 | unknown | ||
| 153 | >; | ||
| 154 | }; | ||
| 155 | |||
| 156 | const buildExecuteWorkflowText = (namespaces: readonly string[] = []): string => | ||
| 157 | [ | ||
| 158 | "Execute TypeScript in sandbox; call tools via discovery workflow.", | ||
| 159 | ...(namespaces.length > 0 | ||
| 160 | ? [ | ||
| 161 | "Available namespaces:", | ||
| 162 | ...namespaces.map((namespace) => `- ${namespace}`), | ||
| 163 | ] | ||
| 164 | : []), | ||
| 165 | "Workflow:", | ||
| 166 | '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });', | ||
| 167 | "2) const details = await tools.describe.tool({ path, includeSchemas: true });", | ||
| 168 | "3) Call selected tools.<path>(input).", | ||
| 169 | '4) To connect a source, call tools.executor.sources.add(...) for MCP, OpenAPI, or GraphQL APIs.', | ||
| 170 | ...EXECUTOR_SOURCES_ADD_HELP_LINES, | ||
| 171 | "5) If execution pauses for interaction, resume it with the returned resumePayload or the available resume flow.", | ||
| 172 | "Do not use fetch; use tools.* only.", | ||
| 173 | ].join("\n"); | ||
| 174 | |||
| 175 | const defaultExecuteDescription = buildExecuteWorkflowText(); | ||
| 176 | |||
| 177 | const loadExecuteDescription = (runtime: SqlControlPlaneRuntime): Promise<string> => | ||
| 178 | runControlPlane( | ||
| 179 | runtime, | ||
| 180 | Effect.gen(function* () { | ||
| 181 | const resolveExecutionEnvironment = yield* RuntimeExecutionResolverService; | ||
| 182 | const environment = yield* resolveExecutionEnvironment({ | ||
| 183 | workspaceId: runtime.localInstallation.workspaceId, | ||
| 184 | accountId: runtime.localInstallation.accountId, | ||
| 185 | executionId: ExecutionIdSchema.make("exec_mcp_help"), | ||
| 186 | }); | ||
| 187 | |||
| 188 | const catalog = environment.catalog as CatalogLike | undefined; | ||
| 189 | if (!catalog) { | ||
| 190 | return defaultExecuteDescription; | ||
| 191 | } | ||
| 192 | |||
| 193 | const namespaces = yield* catalog.listNamespaces({ limit: 200 }).pipe( | ||
| 194 | Effect.map((items) => | ||
| 195 | items.length > 0 | ||
| 196 | ? items.map((item) => item.displayName ?? item.namespace) | ||
| 197 | : ["none discovered yet"], | ||
| 198 | ), | ||
| 199 | Effect.catchAll(() => Effect.succeed(["none discovered yet"])), | ||
| 200 | ); | ||
| 201 | |||
| 202 | return buildExecuteWorkflowText(namespaces); | ||
| 203 | }).pipe(Effect.catchAll(() => Effect.succeed(defaultExecuteDescription))), | ||
| 204 | ); | ||
| 205 | |||
| 131 | const summarizeExecution = (execution: ExecutionEnvelope["execution"]): string => { | 206 | const summarizeExecution = (execution: ExecutionEnvelope["execution"]): string => { |
| 132 | switch (execution.status) { | 207 | switch (execution.status) { |
| 133 | case "completed": | 208 | case "completed": |
| @@ -348,26 +423,16 @@ const driveExecutionWithoutElicitation = async (input: { · 214 unmodified lines | |||
| 348 | return current; | 423 | return current; |
| 349 | }; | 424 | }; |
| 350 | 425 | ||
| 351 | const buildInstallInstructions = (baseUrl: string): string => { | 426 | const createExecutorMcpServer = async (config: { |
| 352 | const mcpUrl = new URL("/mcp", baseUrl).toString(); | ||
| 353 | |||
| 354 | return [ | ||
| 355 | "Run once to install for supported MCP clients (via add-mcp):", | ||
| 356 | `npx add-mcp \"${mcpUrl}\" --transport http --name \"executor\"`, | ||
| 357 | ].join("\n"); | ||
| 358 | }; | ||
| 359 | |||
| 360 | const createExecutorMcpServer = (config: { | ||
| 361 | runtime: SqlControlPlaneRuntime; | 427 | runtime: SqlControlPlaneRuntime; |
| 362 | baseUrl: string; | 428 | }): Promise<McpServer> => { |
| 363 | }): McpServer => { | 429 | const executeDescription = await loadExecuteDescription(config.runtime); |
| 364 | const server = new McpServer( | 430 | const server = new McpServer( |
| 365 | { name: "executor", version: "1.0.0" }, | 431 | { name: "executor", version: "1.0.0" }, |
| 366 | { | 432 | { |
| 367 | capabilities: { | 433 | capabilities: { |
| 368 | tools: {}, | 434 | tools: {}, |
| 369 | }, | 435 | }, |
| 370 | instructions: buildInstallInstructions(config.baseUrl), | ||
| 371 | }, | 436 | }, |
| 372 | ); | 437 | ); |
| 373 | 438 | ||
| @@ -377,10 +442,7 @@ const createExecutorMcpServer = (config: { · 3 unmodified lines | |||
| 377 | const executeTool = server.registerTool( | 442 | const executeTool = server.registerTool( |
| 378 | "execute", | 443 | "execute", |
| 379 | { | 444 | { |
| 380 | description: [ | 445 | description: executeDescription, |
| 381 | "Execute TypeScript against the local executor runtime.", | ||
| 382 | "Use the in-sandbox tool catalog via tools.* rather than direct fetch calls.", | ||
| 383 | ].join("\n"), | ||
| 384 | inputSchema: executeInputSchema, | 446 | inputSchema: executeInputSchema, |
| 385 | }, | 447 | }, |
| 386 | async ({ code }: { code: string }) => { | 448 | async ({ code }: { code: string }) => { |
| @@ -431,7 +493,6 @@ const createExecutorMcpServer = (config: { · 44 unmodified lines | |||
| 431 | }, | 493 | }, |
| 432 | ); | 494 | ); |
| 433 | 495 | ||
| 434 | |||
| 435 | const syncToolAvailability = () => { | 496 | const syncToolAvailability = () => { |
| 436 | if (supportsManagedElicitation(server)) { | 497 | if (supportsManagedElicitation(server)) { |
| 437 | executeTool.enable(); | 498 | executeTool.enable(); |
| @@ -524,9 +585,8 @@ export const createExecutorMcpRequestHandler = ( · 86 unmodified lines | |||
| 524 | }; | 585 | }; |
| 525 | 586 | ||
| 526 | try { | 587 | try { |
| 527 | createdServer = createExecutorMcpServer({ | 588 | createdServer = await createExecutorMcpServer({ |
| 528 | runtime, | 589 | runtime, |
| 529 | baseUrl: new URL(request.url).origin, | ||
| 530 | }); | 590 | }); |
| 531 | await createdServer.connect(transport); | 591 | await createdServer.connect(transport); |
| 532 | const response = await transport.handleRequest(request); | 592 | const response = await transport.handleRequest(request); |
| @@ -28,6 +28,8 @@ import type { · 27 unmodified lines | |||
| 28 | } from "@executor-v3/control-plane"; | 28 | } from "@executor-v3/control-plane"; |
| 29 | import * as Cause from "effect/Cause"; | 29 | import * as Cause from "effect/Cause"; |
| 30 | import * as Effect from "effect/Effect"; | 30 | import * as Effect from "effect/Effect"; |
| 31 | import * as Exit from "effect/Exit"; | ||
| 32 | import * as Runtime from "effect/Runtime"; | ||
| 31 | import * as React from "react"; | 33 | import * as React from "react"; |
| 32 | 34 | ||
| 33 | const DEFAULT_EXECUTOR_API_BASE_URL = "http://127.0.0.1:8788"; | 35 | const DEFAULT_EXECUTOR_API_BASE_URL = "http://127.0.0.1:8788"; |
| @@ -177,6 +179,15 @@ const shouldLogExecutorDevErrors = (): boolean => { · 143 unmodified lines | |||
| 177 | }; | 179 | }; |
| 178 | 180 | ||
| 179 | const describeExecutorDevError = (cause: unknown): Record<string, unknown> => { | 181 | const describeExecutorDevError = (cause: unknown): Record<string, unknown> => { |
| 182 | if (Runtime.isFiberFailure(cause)) { | ||
| 183 | const inner = cause[Runtime.FiberFailureCauseId]; | ||
| 184 | return { | ||
| 185 | name: cause.name, | ||
| 186 | message: cause.message, | ||
| 187 | cause: Cause.pretty(inner), | ||
| 188 | }; | ||
| 189 | } | ||
| 190 | |||
| 180 | if (cause instanceof Error) { | 191 | if (cause instanceof Error) { |
| 181 | return { | 192 | return { |
| 182 | name: cause.name, | 193 | name: cause.name, |
| @@ -198,26 +209,31 @@ const logExecutorDevError = (label: string, details: Record<string, unknown>): v · 15 unmodified lines | |||
| 198 | console.error(`[executor react] ${label}`, details); | 209 | console.error(`[executor react] ${label}`, details); |
| 199 | }; | 210 | }; |
| 200 | 211 | ||
| 201 | const runControlPlane = <A>(input: { | 212 | const runControlPlane = async <A>(input: { |
| 202 | accountId?: string; | 213 | accountId?: string; |
| 203 | execute: (client: ControlPlaneClient) => Effect.Effect<A, unknown, never>; | 214 | execute: (client: ControlPlaneClient) => Effect.Effect<A, unknown, never>; |
| 204 | }): Promise<A> => { | 215 | }): Promise<A> => { |
| 205 | const accountId = input.accountId; | 216 | const accountId = input.accountId; |
| 206 | 217 | ||
| 207 | return Effect.runPromise( | 218 | const exit = await Effect.runPromiseExit( |
| 208 | createControlPlaneClient({ | 219 | createControlPlaneClient({ |
| 209 | baseUrl: apiBaseUrl, | 220 | baseUrl: apiBaseUrl, |
| 210 | ...(accountId !== undefined ? { accountId } : {}), | 221 | ...(accountId !== undefined ? { accountId } : {}), |
| 211 | }).pipe(Effect.flatMap(input.execute)), | 222 | }).pipe(Effect.flatMap(input.execute)), |
| 212 | ).catch((cause) => { | 223 | ); |
| 213 | logExecutorDevError("control-plane request failed", { | 224 | |
| 214 | baseUrl: apiBaseUrl, | 225 | if (Exit.isSuccess(exit)) { |
| 215 | accountId, | 226 | return exit.value; |
| 216 | error: describeExecutorDevError(cause), | 227 | } |
| 217 | cause, | 228 | |
| 218 | }); | 229 | const error = Cause.squash(exit.cause); |
| 219 | throw cause; | 230 | logExecutorDevError("control-plane request failed", { |
| 231 | baseUrl: apiBaseUrl, | ||
| 232 | accountId, | ||
| 233 | error: describeExecutorDevError(error), | ||
| 234 | cause: Cause.pretty(exit.cause), | ||
| 220 | }); | 235 | }); |
| 236 | throw error; | ||
| 221 | }; | 237 | }; |
| 222 | 238 | ||
| 223 | const controlPlaneRequest = <A>(input: { | 239 | const controlPlaneRequest = <A>(input: { |
| @@ -80,7 +80,11 @@ export type LocalExecutorRequestHandler = { · 79 unmodified lines | |||
| 80 | }; | 80 | }; |
| 81 | 81 | ||
| 82 | const disposeRuntime = (runtime: SqlControlPlaneRuntime) => | 82 | const disposeRuntime = (runtime: SqlControlPlaneRuntime) => |
| 83 | Effect.promise(() => runtime.close()).pipe(Effect.orDie); | 83 | Effect.tryPromise({ |
| 84 | try: () => runtime.close(), | ||
| 85 | catch: (cause) => | ||
| 86 | cause instanceof Error ? cause : new Error(String(cause ?? "runtime close failed")), | ||
| 87 | }).pipe(Effect.orDie); | ||
| 84 | 88 | ||
| 85 | const createControlPlaneWebHandler = (runtime: SqlControlPlaneRuntime) => | 89 | const createControlPlaneWebHandler = (runtime: SqlControlPlaneRuntime) => |
| 86 | Effect.acquireRelease( | 90 | Effect.acquireRelease( |
| @@ -92,7 +96,7 @@ const createControlPlaneWebHandler = (runtime: SqlControlPlaneRuntime) => · 5 unmodified lines | |||
| 92 | ), | 96 | ), |
| 93 | ), | 97 | ), |
| 94 | ), | 98 | ), |
| 95 | (handler) => Effect.promise(() => handler.dispose()).pipe(Effect.orDie), | 99 | (handler) => Effect.tryPromise({ try: () => handler.dispose(), catch: (cause) => cause instanceof Error ? cause : new Error(String(cause ?? "web handler dispose failed")) }).pipe(Effect.orDie), |
| 96 | ); | 100 | ); |
| 97 | 101 | ||
| 98 | const safeFilePath = (assetsDir: string, pathname: string): string | null => { | 102 | const safeFilePath = (assetsDir: string, pathname: string): string | null => { |
| @@ -280,7 +284,7 @@ export const createLocalExecutorRequestHandler = ( · 181 unmodified lines | |||
| 280 | const apiHandler = yield* createControlPlaneWebHandler(runtime); | 284 | const apiHandler = yield* createControlPlaneWebHandler(runtime); |
| 281 | const mcpHandler = yield* Effect.acquireRelease( | 285 | const mcpHandler = yield* Effect.acquireRelease( |
| 282 | Effect.sync(() => createExecutorMcpRequestHandler(runtime)), | 286 | Effect.sync(() => createExecutorMcpRequestHandler(runtime)), |
| 283 | (handler) => Effect.promise(() => handler.close()).pipe(Effect.orDie), | 287 | (handler) => Effect.tryPromise({ try: () => handler.close(), catch: (cause) => cause instanceof Error ? cause : new Error(String(cause ?? "mcp handler close failed")) }).pipe(Effect.orDie), |
| 284 | ); | 288 | ); |
| 285 | 289 | ||
| 286 | return { | 290 | return { |
| @@ -389,7 +393,8 @@ export const runLocalExecutorServer = ( · 102 unmodified lines | |||
| 389 | }), | 393 | }), |
| 390 | () => Effect.tryPromise({ | 394 | () => Effect.tryPromise({ |
| 391 | try: () => rm(pidFile, { force: true }), | 395 | try: () => rm(pidFile, { force: true }), |
| 392 | catch: () => undefined, | 396 | catch: (cause) => |
| 397 | cause instanceof Error ? cause : new Error(String(cause ?? "pid file cleanup failed")), | ||
| 393 | }).pipe(Effect.orDie), | 398 | }).pipe(Effect.orDie), |
| 394 | ); | 399 | ); |
| 395 | 400 | ||