History

Run deferred tool syncs off the request on local, self-host and cloudflare

Rhys Sullivan committed 3 months ago

Commit 936f95e1 parent d5459f43 files · +20 −2Browse files

Unified · Split

3 files
  1. apps/host-cloudflare/src/execution.ts +7 −1
  2. apps/host-selfhost/src/execution.ts +8 −1
  3. apps/local/src/executor.ts +5 −0
apps/host-cloudflare/src/execution.ts+7−1
@@ -12,7 +12,7 @@ import { · 11 unmodified lines
1212 } from "@executor-js/api/server";
1313 import { makeDynamicWorkerExecutor } from "@executor-js/runtime-dynamic-worker";
1414 import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs";
15−import { env } from "cloudflare:workers";
15+import { env, waitUntil } from "cloudflare:workers";
1616
1717 import type { CloudflareConfig } from "./config";
1818 import { makeCloudflarePlugins } from "./plugins";
@@ -56,6 +56,12 @@ export const makeCloudflareHostConfig = (config: CloudflareConfig): Layer.Layer< · 37 unmodified lines
5656 allowLocalNetwork: config.allowLocalNetwork,
5757 webBaseUrl: config.webBaseUrl,
5858 oauthCallbackPath: "/api/oauth/callback",
59+ // A Worker, so a tools read's speculative catalog refresh needs `waitUntil`
60+ // to survive the response at all. Unlike cloud's Hyperdrive plane there is
61+ // nothing to drain it before: the storage here is a D1 binding read
62+ // straight off `env`, with no pool and no close finalizer, so a detached
63+ // batch still has everything it needs after the response is written.
64+ deferToolSync: (task) => Effect.sync(() => waitUntil(Effect.runPromise(task))),
5965 });
6066
6167 /**
apps/host-selfhost/src/execution.ts+8−1
@@ -1,4 +1,4 @@
1−import { Layer } from "effect";
1+import { Effect, Layer } from "effect";
22
33 import {
44 CodeExecutorProvider,
@@ -60,6 +60,13 @@ export const SelfHostHostConfig: Layer.Layer<HostConfig> = Layer.sync(HostConfig · 55 unmodified lines
6060 event.kind === "added" ? "integration_added" : "integration_removed",
6161 { plugin_key: event.pluginKey },
6262 ),
63+ // Self-host is one long-lived process over one long-lived libSQL handle, so
64+ // a speculative catalog refresh just goes to a detached fiber: the handle
65+ // it needs outlives every request, and there is no isolate to keep alive.
66+ // Detached, not `forkScoped` — the request scope this is called from closes
67+ // as soon as the response is written, which is precisely what the work has
68+ // to outlive. The task is total by contract, so nothing escapes the fork.
69+ deferToolSync: (task) => Effect.asVoid(Effect.forkDetach(task)),
6370 };
6471 });
6572
apps/local/src/executor.ts+5−0
@@ -201,6 +201,11 @@ const createLocalExecutorLayer = (options: LocalExecutorOptions = {}) => { · 200 unmodified lines
201201 event.kind === "added" ? "integration_added" : "integration_removed",
202202 { plugin_key: event.pluginKey },
203203 ),
204+ // The daemon owns its sqlite handle for its whole lifetime, so a tools
205+ // read's speculative catalog refresh goes to a detached fiber and the
206+ // read answers without waiting on an MCP server's handshake. The task
207+ // is total by contract, so nothing escapes the fork.
208+ deferToolSync: (task) => Effect.asVoid(Effect.forkDetach(task)),
204209 onElicitation: "accept-all",
205210 oauthEndpointUrlPolicy: { allowHttp: true },
206211 // EXPLICIT OAuth callback — the daemon serves the v2 `/api/oauth/callback`

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t