History

mcp-session: cache executor.tools.list on the DO

tools.search loads the full tool list on every call to score against it. The MCP session DO survives the whole CLI session, so repeated searches share one DB fetch. Wrap executor.tools.list in a plain Map keyed by JSON.stringify(filter); DO idle timeout (5m) caps staleness. Subsequent searches drop from ~1.5s p50 to ~0ms.

Rhys Sullivan committed 6 months ago

Commit 388f5321 parent 0bb20cb1 file · +32 −1Browse files

Unified · Split

apps/cloud/src/services/execution-stack.ts+32−1
@@ -9,22 +9,53 @@ import { Effect } from "effect"; · 8 unmodified lines
99
1010 import { createExecutionEngine } from "@executor/execution";
1111 import { makeDynamicWorkerExecutor } from "@executor/runtime-dynamic-worker";
12+import type { Tool, ToolListFilter } from "@executor/sdk";
1213
1314 import { withExecutionUsageTracking } from "../api/execution-usage";
1415 import { AutumnService } from "./autumn";
1516 import { createScopedExecutor } from "./executor";
1617
18+// In-memory tools.list cache on the DO. `tools.search` scans the full
19+// list on every call, so caching it collapses the N calls in a session
20+// into one DB fetch. DO lifetime caps cache staleness at
21+// SESSION_TIMEOUT_MS (5m idle) — no TTL needed.
22+const makeToolsListCache = <E, R>(
23+ inner: (filter?: ToolListFilter) => Effect.Effect<readonly Tool[], E, R>,
24+) => {
25+ const cache = new Map<string, readonly Tool[]>();
26+ return (filter?: ToolListFilter) =>
27+ Effect.gen(function* () {
28+ const key = JSON.stringify(filter ?? null);
29+ const hit = cache.get(key);
30+ if (hit) {
31+ yield* Effect.annotateCurrentSpan({ "cache.state": "hit" });
32+ return hit;
33+ }
34+ const value = yield* inner(filter);
35+ cache.set(key, value);
36+ yield* Effect.annotateCurrentSpan({ "cache.state": "miss" });
37+ return value;
38+ }).pipe(Effect.withSpan("executor.tools.list.cached"));
39+};
40+
1741 export const makeExecutionStack = (
1842 userId: string,
1943 organizationId: string,
2044 organizationName: string,
2145 ) =>
2246 Effect.gen(function* () {
23− const executor = yield* createScopedExecutor(
47+ const rawExecutor = yield* createScopedExecutor(
2448 userId,
2549 organizationId,
2650 organizationName,
2751 ).pipe(Effect.withSpan("McpSessionDO.createScopedExecutor"));
52+ const executor = {
53+ ...rawExecutor,
54+ tools: {
55+ ...rawExecutor.tools,
56+ list: makeToolsListCache(rawExecutor.tools.list),
57+ },
58+ };
2859 const codeExecutor = makeDynamicWorkerExecutor({ loader: env.LOADER });
2960 const autumn = yield* AutumnService;
3061 const engine = withExecutionUsageTracking(