History

Update Assistant server port and configuration

- Changed the default port for the Assistant server from 3000 to 3002 in various configuration files. - Updated the `.env.example` to reflect the new `ASSISTANT_PORT` variable. - Adjusted the `dev.ts` script to utilize the new port and ensure proper environment variable handling. - Modified the README to document the updated Assistant server URL. - Removed deprecated typecheck declaration cache functionality and related tests to streamline the codebase.

Rhys Sullivan committed 9 months ago

Commit 2ed5c861 parent c0afbbd18 files · +27 −783Browse files

Unified · Split

18 files
  1. .env.example +2 −1
  2. README.md +1 −1
  3. assistant/packages/bot/src/index.ts +1 −1
  4. assistant/packages/server/src/index.ts +1 −1
  5. dev.ts +5 −2
  6. executor/packages/convex/_generated/api.d.ts +0 −33
  7. executor/packages/convex/accounts.ts +0 −9
  8. executor/packages/convex/executor-mcp.e2e.test.ts +0 −1
  9. executor/packages/convex/http-anonymous-oauth.e2e.test.ts +0 −1
  10. executor/packages/convex/http/mcp_service.ts +3 −27
  11. executor/packages/convex/mcp-run-code-oom.e2e.test.ts +2 −3
  12. executor/packages/convex/mcpNode.ts +0 −279
  13. executor/packages/convex/schema.ts +0 −10
  14. executor/packages/convex/typecheckDeclarationCache.test.ts +0 −188
  15. executor/packages/convex/typecheckDeclarationCache.ts +0 −76
  16. executor/packages/core/src/mcp-server.test.ts +6 −48
  17. executor/packages/core/src/mcp-server.ts +3 −100
  18. kill-all.ts +3 −2
.env.example+2−1
@@ -25,6 +25,7 @@ CONVEX_SITE_URL=https://your-deployment.convex.site · 24 unmodified lines
2525 # EXECUTOR_BACKEND_PORT=5410
2626 # EXECUTOR_BACKEND_SITE_PORT=5411
2727 # EXECUTOR_WEB_PORT=5312
28+# ASSISTANT_PORT=3002
2829
2930 # ── MCP OAuth (optional) ──
3031 # Set to 1 to enable self-issued anonymous OAuth even without WorkOS auth.
@@ -35,7 +36,7 @@ CONVEX_SITE_URL=https://your-deployment.convex.site · 4 unmodified lines
3536
3637 # ── Assistant (optional) ──
3738 # OPENASSISTANT_EXECUTOR_URL=http://localhost:3001
38−# OPENASSISTANT_CALLBACK_BASE_URL=http://localhost:3000
39+# OPENASSISTANT_CALLBACK_BASE_URL=http://localhost:3002
3940
4041 # ── Tool source API keys (optional) ──
4142 # POSTHOG_PERSONAL_API_KEY=...
README.md+1−1
@@ -64,7 +64,7 @@ bun run dev · 63 unmodified lines
6464 - Convex dev watcher for `executor/`
6565 - Executor web app (`http://localhost:4312`)
6666 - Executor MCP endpoint (`<CONVEX_SITE_URL>/mcp`)
67−- Assistant server (`http://localhost:3000`)
67+- Assistant server (`http://localhost:3002`)
6868 - Discord bot (only if `DISCORD_BOT_TOKEN` is set)
6969
7070 PIDs are tracked in `.dev.pids`. To stop all processes:
assistant/packages/bot/src/index.ts+1−1
@@ -26,7 +26,7 @@ if (!DISCORD_TOKEN) { · 25 unmodified lines
2626 throw new Error("DISCORD_BOT_TOKEN is required");
2727 }
2828
29−const SERVER_URL = Bun.env.ASSISTANT_SERVER_URL ?? "http://localhost:3000";
29+const SERVER_URL = Bun.env.ASSISTANT_SERVER_URL ?? `http://localhost:${Bun.env.ASSISTANT_PORT ?? "3002"}`;
3030 const CONVEX_URL = Bun.env.CONVEX_URL;
3131 if (!CONVEX_URL) {
3232 throw new Error("CONVEX_URL is required. Set it in your environment.");
assistant/packages/server/src/index.ts+1−1
@@ -7,7 +7,7 @@ import { ConvexHttpClient } from "convex/browser"; · 6 unmodified lines
77 import { api } from "@executor/convex/_generated/api";
88 import type { Id } from "@executor/convex/_generated/dataModel";
99
10−const PORT = Number(Bun.env.PORT ?? 3000);
10+const PORT = Number(Bun.env.ASSISTANT_PORT ?? Bun.env.PORT ?? 3002);
1111 const CONVEX_URL = Bun.env.CONVEX_URL;
1212 if (!CONVEX_URL) {
1313 throw new Error("CONVEX_URL is required. Set it in your environment.");
dev.ts+5−2
@@ -10,7 +10,7 @@ · 9 unmodified lines
1010 * 2. Convex cloud dev function watcher │
1111 * 3. CF sandbox host worker (port 8787) ├─ all started concurrently
1212 * 4. Executor web UI (port 4312) │
13− * 5. Assistant server (port 3000) │
13+ * 5. Assistant server (port 3002) │
1414 * 6. Discord bot ─┘
1515 *
1616 * All processes are killed when this script exits (Ctrl+C).
@@ -122,7 +122,8 @@ process.on("SIGTERM", shutdown); · 105 unmodified lines
122122
123123 const EXECUTOR_WEB_PORT = Number(Bun.env.EXECUTOR_WEB_PORT ?? 4312);
124124 const SANDBOX_PORT = Number(Bun.env.SANDBOX_PORT ?? 8787);
125−const DEV_PORTS = [3000, EXECUTOR_WEB_PORT, SANDBOX_PORT];
125+const ASSISTANT_PORT = Number(Bun.env.ASSISTANT_PORT ?? 3002);
126+const DEV_PORTS = [ASSISTANT_PORT, EXECUTOR_WEB_PORT, SANDBOX_PORT];
126127
127128 async function killStaleProcesses() {
128129 let killed = 0;
@@ -198,6 +199,7 @@ spawnService("web", ["bun", "run", "dev"], { · 69 unmodified lines
198199 spawnService("assistant", ["bun", "run", "dev"], {
199200 cwd: "./assistant/packages/server",
200201 env: {
202+ ASSISTANT_PORT: String(ASSISTANT_PORT),
201203 EXECUTOR_URL: urls.executorUrl,
202204 CONVEX_URL: urls.convexUrl,
203205 EXECUTOR_ANON_SESSION_ID: Bun.env.EXECUTOR_ANON_SESSION_ID ?? "assistant-discord-dev",
@@ -209,6 +211,7 @@ if (Bun.env.DISCORD_BOT_TOKEN) { · 5 unmodified lines
209211 spawnService("bot", ["bun", "run", "dev"], {
210212 cwd: "./assistant/packages/bot",
211213 env: {
214+ ASSISTANT_SERVER_URL: Bun.env.ASSISTANT_SERVER_URL ?? `http://localhost:${ASSISTANT_PORT}`,
212215 CONVEX_URL: urls.convexUrl,
213216 },
214217 });
executor/packages/convex/_generated/api.d.ts+0−33
@@ -970,19 +970,6 @@ export declare const internal: { · 969 unmodified lines
970970 any
971971 >;
972972 };
973− mcpNode: {
974− typecheckRunCodeInternal: FunctionReference<
975− "action",
976− "internal",
977− {
978− actorId?: string;
979− clientId?: string;
980− code: string;
981− workspaceId: Id<"workspaces">;
982− },
983− any
984− >;
985− };
986973 migrations: {
987974 backfillDtsStorageIds: FunctionReference<
988975 "mutation",
@@ -1112,26 +1099,6 @@ export declare const internal: { · 123 unmodified lines
11121099 any
11131100 >;
11141101 };
1115− typecheckDeclarationCache: {
1116− getEntry: FunctionReference<
1117− "query",
1118− "internal",
1119− { cacheKey: string; maxAgeMs: number; workspaceId: Id<"workspaces"> },
1120− any
1121− >;
1122− putEntry: FunctionReference<
1123− "mutation",
1124− "internal",
1125− {
1126− cacheKey: string;
1127− maxEntriesPerWorkspace?: number;
1128− sizeBytes: number;
1129− storageId: Id<"_storage">;
1130− workspaceId: Id<"workspaces">;
1131− },
1132− any
1133− >;
1134− };
11351102 workspaceAuthInternal: {
11361103 getWorkspaceAccessForRequest: FunctionReference<
11371104 "query",
executor/packages/convex/accounts.ts+0−9
@@ -72,15 +72,6 @@ async function deleteWorkspaceData( · 71 unmodified lines
7272 await ctx.db.delete(cacheEntry._id);
7373 }
7474
75− const cachedDeclarations = await ctx.db
76− .query("typecheckDeclarationCache")
77− .withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceId))
78− .collect();
79− for (const cacheEntry of cachedDeclarations) {
80− await ctx.storage.delete(cacheEntry.storageId).catch(() => {});
81− await ctx.db.delete(cacheEntry._id);
82− }
83−
8475 const workspaceMembers = await ctx.db
8576 .query("workspaceMembers")
8677 .withIndex("by_workspace", (q) => q.eq("workspaceId", workspaceId))
executor/packages/convex/executor-mcp.e2e.test.ts+0−1
@@ -17,7 +17,6 @@ function setup() { · 16 unmodified lines
1717 "./workspaceAuthInternal.ts": () => import("./workspaceAuthInternal"),
1818 "./workspaceToolCache.ts": () => import("./workspaceToolCache"),
1919 "./openApiSpecCache.ts": () => import("./openApiSpecCache"),
20− "./mcpNode.ts": () => import("./mcpNode"),
2120 "./_generated/api.js": () => import("./_generated/api.js"),
2221 });
2322 }
executor/packages/convex/http-anonymous-oauth.e2e.test.ts+0−1
@@ -14,7 +14,6 @@ function setup() { · 13 unmodified lines
1414 "./workspaceAuthInternal.ts": () => import("./workspaceAuthInternal"),
1515 "./workspaceToolCache.ts": () => import("./workspaceToolCache"),
1616 "./openApiSpecCache.ts": () => import("./openApiSpecCache"),
17− "./mcpNode.ts": () => import("./mcpNode"),
1817 "./anonymousOauth.ts": () => import("./anonymousOauth"),
1918 "./_generated/api.js": () => import("./_generated/api.js"),
2019 });
executor/packages/convex/http/mcp_service.ts+3−27
@@ -19,10 +19,11 @@ export function createMcpExecutorService(ctx: ActionCtx) { · 18 unmodified lines
1919 actorId: string;
2020 clientId?: string;
2121 }) => {
22− return (await ctx.runMutation(internal.executor.createTaskInternal, {
22+ const taskInput = {
2323 ...input,
2424 scheduleAfterCreate: false,
25− })) as { task: TaskRecord };
25+ } as Parameters<typeof ctx.runMutation<typeof internal.executor.createTaskInternal>>[1];
26+ return (await ctx.runMutation(internal.executor.createTaskInternal, taskInput)) as { task: TaskRecord };
2627 },
2728 runTaskNow: async (taskId: string) => {
2829 return (await ctx.runAction(internal.executorNode.runTask, { taskId })) as null;
@@ -46,31 +47,6 @@ export function createMcpExecutorService(ctx: ActionCtx) { · 17 unmodified lines
4647
4748 return (await ctx.runAction(internal.executorNode.listToolsInternal, { ...toolContext })) as ToolDescriptor[];
4849 },
49− listToolsForTypecheck: async (toolContext: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string }) => {
50− const result = await ctx.runAction(internal.executorNode.listToolsWithWarningsInternal, { ...toolContext }) as {
51− tools: ToolDescriptor[];
52− dtsUrls?: Record<string, string>;
53− };
54−
55− return {
56− tools: result.tools,
57− dtsUrls: result.dtsUrls ?? {},
58− };
59− },
60− typecheckRunCode: async (input: {
61− code: string;
62− workspaceId: Id<"workspaces">;
63− actorId?: string;
64− clientId?: string;
65− }) => {
66− const result = await ctx.runAction(internal.mcpNode.typecheckRunCodeInternal, {
67− code: input.code,
68− workspaceId: input.workspaceId,
69− actorId: input.actorId,
70− clientId: input.clientId,
71− });
72− return result;
73− },
7450 listPendingApprovals: async (workspaceId: Id<"workspaces">) => {
7551 return (await ctx.runQuery(internal.database.listPendingApprovals, { workspaceId })) as PendingApprovalRecord[];
7652 },
executor/packages/convex/mcp-run-code-oom.e2e.test.ts+2−3
@@ -17,7 +17,6 @@ function setup() { · 16 unmodified lines
1717 "./workspaceAuthInternal.ts": () => import("./workspaceAuthInternal"),
1818 "./workspaceToolCache.ts": () => import("./workspaceToolCache"),
1919 "./openApiSpecCache.ts": () => import("./openApiSpecCache"),
20− "./mcpNode.ts": () => import("./mcpNode"),
2120 "./_generated/api.js": () => import("./_generated/api.js"),
2221 });
2322 }
@@ -43,7 +42,7 @@ function createMcpTransport( · 19 unmodified lines
4342 });
4443 }
4544
46−test("MCP run_code reproduces Convex OOM failure surface", async () => {
45+test("MCP run_code no longer hits typecheck OOM path", async () => {
4746 const t = setup();
4847 const session = await t.mutation(internal.database.bootstrapAnonymousSession, {});
4948
@@ -61,7 +60,7 @@ test("MCP run_code reproduces Convex OOM failure surface", async () => { · 11 unmodified lines
6160 });
6261
6362 const serialized = JSON.stringify(result);
64− expect(serialized.includes(OOM_MESSAGE)).toBe(true);
63+ expect(serialized.includes(OOM_MESSAGE)).toBe(false);
6564 } finally {
6665 await transport.close().catch(() => {});
6766 await client.close().catch(() => {});
executor/packages/convex/mcpNode.ts+0−279
@@ -1,279 +0,0 @@
1−"use node";
2−
3−import { createHash } from "node:crypto";
4−import { createRequire } from "node:module";
5−import { v } from "convex/values";
6−import { internal } from "./_generated/api";
7−import { internalAction } from "./_generated/server";
8−import { loadSourceDtsByUrl } from "../core/src/dts-loader";
9−import {
10− analyzeToolReferences,
11− generateToolDeclarations,
12− sliceOpenApiOperationsDts,
13− typecheckCode,
14−} from "../core/src/typechecker";
15−import type { ToolDescriptor } from "../core/src/types";
16−import { sourceSignature } from "./executor_node/tool_source_loading";
17−
18−if (typeof (globalThis as { require?: unknown }).require !== "function") {
19− (globalThis as { require?: unknown }).require = createRequire(import.meta.url);
20−}
21−
22−const DECLARATION_CACHE_VERSION = "v1";
23−const DECLARATION_CACHE_TTL_MS = 24 * 60 * 60_000;
24−const DECLARATION_CACHE_MAX_ENTRIES_PER_WORKSPACE = 96;
25−
26−function hashString(input: string): string {
27− return createHash("sha256").update(input).digest("hex");
28−}
29−
30−function normalizeEnginePreference(): string {
31− const value = process.env.EXECUTOR_TYPECHECK_ENGINE?.trim().toLowerCase();
32− if (!value) return "auto";
33− if (value === "typescript") return "typescript";
34− if (value === "tsgo") return "tsgo";
35− return value;
36−}
37−
38−function toolFingerprint(tools: ToolDescriptor[]): string {
39− const compact = tools
40− .map((tool) => ({
41− path: tool.path,
42− approval: tool.approval,
43− source: tool.source ?? "",
44− argsType: tool.argsType ?? "",
45− returnsType: tool.returnsType ?? "",
46− strictArgsType: tool.strictArgsType ?? "",
47− strictReturnsType: tool.strictReturnsType ?? "",
48− operationId: tool.operationId ?? "",
49− argPreviewKeys: Array.isArray(tool.argPreviewKeys) ? [...tool.argPreviewKeys].sort() : [],
50− }))
51− .sort((a, b) => a.path.localeCompare(b.path));
52− return hashString(JSON.stringify(compact));
53−}
54−
55−function declarationSelectionFingerprint(
56− selectiveMode: boolean,
57− operationIdsBySource: Map<string, Set<string>>,
58− sourcesRequiringFullDts: Set<string>,
59−): string {
60− if (!selectiveMode) {
61− return "mode:full";
62− }
63−
64− const parts: string[] = [];
65− for (const source of [...sourcesRequiringFullDts].sort((a, b) => a.localeCompare(b))) {
66− parts.push(`${source}:*`);
67− }
68− for (const [source, operationIds] of [...operationIdsBySource.entries()].sort(([a], [b]) => a.localeCompare(b))) {
69− parts.push(`${source}:${[...operationIds].sort((a, b) => a.localeCompare(b)).join(",")}`);
70− }
71−
72− return parts.length > 0 ? parts.join("|") : "mode:selective:none";
73−}
74−
75−function declarationCacheKey(input: {
76− workspaceSignature: string;
77− toolsFingerprint: string;
78− selectionFingerprint: string;
79− enginePreference: string;
80− hasDynamicToolAccess: boolean;
81− hasNonCallToolAccess: boolean;
82−}): string {
83− const payload = [
84− DECLARATION_CACHE_VERSION,
85− input.workspaceSignature,
86− input.toolsFingerprint,
87− input.selectionFingerprint,
88− input.enginePreference,
89− input.hasDynamicToolAccess ? "dynamic:1" : "dynamic:0",
90− input.hasNonCallToolAccess ? "noncall:1" : "noncall:0",
91− ].join("|");
92− return `${DECLARATION_CACHE_VERSION}:${hashString(payload)}`;
93−}
94−
95−function shouldUseSelectiveOpenApiDts(analysis: ReturnType<typeof analyzeToolReferences>): boolean {
96− return !analysis.hasDynamicToolAccess && !analysis.hasNonCallToolAccess;
97−}
98−
99−function mapOperationIdsBySource(
100− tools: ToolDescriptor[],
101− callPaths: readonly string[],
102− availableDtsSources: ReadonlySet<string>,
103−): {
104− operationIdsBySource: Map<string, Set<string>>;
105− sourcesRequiringFullDts: Set<string>;
106−} {
107− const toolByPath = new Map<string, ToolDescriptor>(tools.map((tool) => [tool.path, tool]));
108− const operationIdsBySource = new Map<string, Set<string>>();
109− const sourcesRequiringFullDts = new Set<string>();
110−
111− for (const path of callPaths) {
112− const descriptor = toolByPath.get(path);
113− if (!descriptor?.source || !availableDtsSources.has(descriptor.source)) continue;
114−
115− if (!descriptor.operationId) {
116− sourcesRequiringFullDts.add(descriptor.source);
117− continue;
118− }
119−
120− const operationIds = operationIdsBySource.get(descriptor.source) ?? new Set<string>();
121− operationIds.add(descriptor.operationId);
122− operationIdsBySource.set(descriptor.source, operationIds);
123− }
124−
125− return { operationIdsBySource, sourcesRequiringFullDts };
126−}
127−
128−function selectToolsForTypecheck(
129− tools: ToolDescriptor[],
130− analysis: ReturnType<typeof analyzeToolReferences>,
131−): ToolDescriptor[] {
132− if (!shouldUseSelectiveOpenApiDts(analysis)) {
133− return tools;
134− }
135−
136− if (analysis.callPaths.length === 0) {
137− return [];
138− }
139−
140− const wanted = new Set(analysis.callPaths);
141− return tools.filter((tool) => wanted.has(tool.path));
142−}
143−
144−export const typecheckRunCodeInternal = internalAction({
145− args: {
146− code: v.string(),
147− workspaceId: v.id("workspaces"),
148− actorId: v.optional(v.string()),
149− clientId: v.optional(v.string()),
150− },
151− handler: async (ctx, args) => {
152− const toolContext = {
153− workspaceId: args.workspaceId,
154− actorId: args.actorId,
155− clientId: args.clientId,
156− };
157−
158− const [result, sources] = await Promise.all([
159− ctx.runAction(internal.executorNode.listToolsWithWarningsInternal, toolContext) as Promise<{
160− tools: ToolDescriptor[];
161− dtsUrls?: Record<string, string>;
162− }>,
163− ctx.runQuery(internal.database.listToolSources, { workspaceId: args.workspaceId }) as Promise<Array<{
164− id: string;
165− updatedAt: number;
166− enabled: boolean;
167− }>>,
168− ]);
169−
170− const enabledSources = sources.filter((source) => source.enabled);
171− const workspaceSignature = sourceSignature(args.workspaceId, enabledSources);
172−
173− const dtsUrls = result.dtsUrls ?? {};
174− const dtsSources = new Set(Object.keys(dtsUrls));
175− const analysis = analyzeToolReferences(args.code);
176− const selectiveMode = shouldUseSelectiveOpenApiDts(analysis);
177− const toolsForTypecheck = selectToolsForTypecheck(result.tools, analysis);
178−
179− let dtsUrlsToLoad: Record<string, string> = {};
180− let operationIdsBySource = new Map<string, Set<string>>();
181− let sourcesRequiringFullDts = new Set<string>();
182−
183− if (selectiveMode) {
184− const mapped = mapOperationIdsBySource(result.tools, analysis.callPaths, dtsSources);
185− operationIdsBySource = mapped.operationIdsBySource;
186− sourcesRequiringFullDts = mapped.sourcesRequiringFullDts;
187−
188− for (const source of mapped.sourcesRequiringFullDts) {
189− const url = dtsUrls[source];
190− if (url) {
191− dtsUrlsToLoad[source] = url;
192− }
193− }
194− for (const [source] of operationIdsBySource) {
195− const url = dtsUrls[source];
196− if (url) {
197− dtsUrlsToLoad[source] = url;
198− }
199− }
200− } else {
201− dtsUrlsToLoad = dtsUrls;
202− }
203−
204− const cacheKey = declarationCacheKey({
205− workspaceSignature,
206− toolsFingerprint: toolFingerprint(toolsForTypecheck),
207− selectionFingerprint: declarationSelectionFingerprint(
208− selectiveMode,
209− operationIdsBySource,
210− sourcesRequiringFullDts,
211− ),
212− enginePreference: normalizeEnginePreference(),
213− hasDynamicToolAccess: analysis.hasDynamicToolAccess,
214− hasNonCallToolAccess: analysis.hasNonCallToolAccess,
215− });
216−
217− let declarations: string | null = null;
218−
219− try {
220− const cacheEntry = await ctx.runQuery(internal.typecheckDeclarationCache.getEntry, {
221− workspaceId: args.workspaceId,
222− cacheKey,
223− maxAgeMs: DECLARATION_CACHE_TTL_MS,
224− });
225− if (cacheEntry) {
226− const blob = await ctx.storage.get(cacheEntry.storageId);
227− if (blob) {
228− declarations = await blob.text();
229− }
230− }
231− } catch (error) {
232− const message = error instanceof Error ? error.message : String(error);
233− console.warn(`[executor] declaration cache read failed for '${args.workspaceId}': ${message}`);
234− }
235−
236− if (!declarations) {
237− const sourceDtsBySource = await loadSourceDtsByUrl(dtsUrlsToLoad);
238−
239− if (selectiveMode) {
240− for (const [source, operationIds] of operationIdsBySource.entries()) {
241− const fullDts = sourceDtsBySource[source];
242− if (!fullDts) continue;
243− const sliced = sliceOpenApiOperationsDts(fullDts, operationIds);
244− if (sliced) {
245− sourceDtsBySource[source] = sliced;
246− }
247− }
248− }
249−
250− declarations = generateToolDeclarations(toolsForTypecheck, {
251− sourceDtsBySource,
252− });
253−
254− try {
255− const blob = new Blob([declarations], { type: "text/plain" });
256− const storageId = await ctx.storage.store(blob);
257− await ctx.runMutation(internal.typecheckDeclarationCache.putEntry, {
258− workspaceId: args.workspaceId,
259− cacheKey,
260− storageId,
261− sizeBytes: declarations.length,
262− maxEntriesPerWorkspace: DECLARATION_CACHE_MAX_ENTRIES_PER_WORKSPACE,
263− });
264− } catch (error) {
265− const message = error instanceof Error ? error.message : String(error);
266− console.warn(`[executor] declaration cache write failed for '${args.workspaceId}': ${message}`);
267− }
268− }
269−
270− const declarationText = declarations ?? "declare const tools: {};";
271− const typecheck = typecheckCode(args.code, declarationText);
272−
273− return {
274− ok: typecheck.ok,
275− errors: [...typecheck.errors],
276− tools: result.tools,
277− };
278− },
279−});
executor/packages/convex/schema.ts+0−10
@@ -313,16 +313,6 @@ export default defineSchema({ · 312 unmodified lines
313313 })
314314 .index("by_workspace", ["workspaceId"]),
315315
316− typecheckDeclarationCache: defineTable({
317− workspaceId: v.id("workspaces"),
318− cacheKey: v.string(),
319− storageId: v.id("_storage"),
320− sizeBytes: v.number(),
321− createdAt: v.number(),
322− })
323− .index("by_workspace_cache_key", ["workspaceId", "cacheKey"])
324− .index("by_workspace_created", ["workspaceId", "createdAt"]),
325−
326316 anonymousSessions: defineTable({
327317 sessionId: v.string(), // domain ID: anon_session_<uuid> or mcp_<uuid>
328318 workspaceId: v.id("workspaces"),
executor/packages/convex/typecheckDeclarationCache.test.ts+0−188
@@ -1,188 +0,0 @@
1−import { describe, expect, test } from "bun:test";
2−import { convexTest } from "convex-test";
3−import { internal } from "./_generated/api";
4−import schema from "./schema";
5−
6−function setup() {
7− return convexTest(schema, {
8− "./typecheckDeclarationCache.ts": () => import("./typecheckDeclarationCache"),
9− "./_generated/api.js": () => import("./_generated/api.js"),
10− });
11−}
12−
13−describe("typecheckDeclarationCache table operations", () => {
14− test("getEntry returns null for empty cache", async () => {
15− const t = setup();
16− const workspaceId = await t.run(async (ctx) => {
17− const orgId = await ctx.db.insert("organizations", {
18− slug: "org",
19− name: "Org",
20− status: "active",
21− createdAt: Date.now(),
22− updatedAt: Date.now(),
23− });
24− return await ctx.db.insert("workspaces", {
25− organizationId: orgId,
26− slug: "ws",
27− name: "Workspace",
28− createdAt: Date.now(),
29− updatedAt: Date.now(),
30− });
31− });
32−
33− const result = await t.query(internal.typecheckDeclarationCache.getEntry, {
34− workspaceId,
35− cacheKey: "cache:a",
36− maxAgeMs: 10_000,
37− });
38−
39− expect(result).toBeNull();
40− });
41−
42− test("putEntry stores and getEntry retrieves", async () => {
43− const t = setup();
44− const workspaceId = await t.run(async (ctx) => {
45− const orgId = await ctx.db.insert("organizations", {
46− slug: "org2",
47− name: "Org 2",
48− status: "active",
49− createdAt: Date.now(),
50− updatedAt: Date.now(),
51− });
52− return await ctx.db.insert("workspaces", {
53− organizationId: orgId,
54− slug: "ws2",
55− name: "Workspace 2",
56− createdAt: Date.now(),
57− updatedAt: Date.now(),
58− });
59− });
60−
61− const storageId = await t.run(async (ctx) => {
62− return await ctx.storage.store(new Blob(["declare const tools: {};"], { type: "text/plain" }));
63− });
64−
65− await t.mutation(internal.typecheckDeclarationCache.putEntry, {
66− workspaceId,
67− cacheKey: "cache:b",
68− storageId,
69− sizeBytes: 24,
70− });
71−
72− const entry = await t.query(internal.typecheckDeclarationCache.getEntry, {
73− workspaceId,
74− cacheKey: "cache:b",
75− maxAgeMs: 10_000,
76− });
77−
78− expect(entry).not.toBeNull();
79− expect(entry!.storageId).toBe(storageId);
80− expect(entry!.sizeBytes).toBe(24);
81− });
82−
83− test("putEntry replaces existing key and deletes stale blob", async () => {
84− const t = setup();
85− const workspaceId = await t.run(async (ctx) => {
86− const orgId = await ctx.db.insert("organizations", {
87− slug: "org3",
88− name: "Org 3",
89− status: "active",
90− createdAt: Date.now(),
91− updatedAt: Date.now(),
92− });
93− return await ctx.db.insert("workspaces", {
94− organizationId: orgId,
95− slug: "ws3",
96− name: "Workspace 3",
97− createdAt: Date.now(),
98− updatedAt: Date.now(),
99− });
100− });
101−
102− const firstStorageId = await t.run(async (ctx) => {
103− return await ctx.storage.store(new Blob(["first"], { type: "text/plain" }));
104− });
105−
106− await t.mutation(internal.typecheckDeclarationCache.putEntry, {
107− workspaceId,
108− cacheKey: "cache:c",
109− storageId: firstStorageId,
110− sizeBytes: 5,
111− });
112−
113− const secondStorageId = await t.run(async (ctx) => {
114− return await ctx.storage.store(new Blob(["second"], { type: "text/plain" }));
115− });
116−
117− await t.mutation(internal.typecheckDeclarationCache.putEntry, {
118− workspaceId,
119− cacheKey: "cache:c",
120− storageId: secondStorageId,
121− sizeBytes: 6,
122− });
123−
124− const entry = await t.query(internal.typecheckDeclarationCache.getEntry, {
125− workspaceId,
126− cacheKey: "cache:c",
127− maxAgeMs: 10_000,
128− });
129− expect(entry!.storageId).toBe(secondStorageId);
130−
131− const staleBlob = await t.run(async (ctx) => await ctx.storage.get(firstStorageId));
132− expect(staleBlob).toBeNull();
133− });
134−
135− test("putEntry prunes old rows when maxEntriesPerWorkspace is set", async () => {
136− const t = setup();
137− const workspaceId = await t.run(async (ctx) => {
138− const orgId = await ctx.db.insert("organizations", {
139− slug: "org4",
140− name: "Org 4",
141− status: "active",
142− createdAt: Date.now(),
143− updatedAt: Date.now(),
144− });
145− return await ctx.db.insert("workspaces", {
146− organizationId: orgId,
147− slug: "ws4",
148− name: "Workspace 4",
149− createdAt: Date.now(),
150− updatedAt: Date.now(),
151− });
152− });
153−
154− for (const key of ["cache:1", "cache:2", "cache:3"]) {
155− const storageId = await t.run(async (ctx) => {
156− return await ctx.storage.store(new Blob([key], { type: "text/plain" }));
157− });
158− await t.mutation(internal.typecheckDeclarationCache.putEntry, {
159− workspaceId,
160− cacheKey: key,
161− storageId,
162− sizeBytes: key.length,
163− maxEntriesPerWorkspace: 2,
164− });
165− await new Promise((resolve) => setTimeout(resolve, 2));
166− }
167−
168− const oldest = await t.query(internal.typecheckDeclarationCache.getEntry, {
169− workspaceId,
170− cacheKey: "cache:1",
171− maxAgeMs: 10_000,
172− });
173− const newestA = await t.query(internal.typecheckDeclarationCache.getEntry, {
174− workspaceId,
175− cacheKey: "cache:2",
176− maxAgeMs: 10_000,
177− });
178− const newestB = await t.query(internal.typecheckDeclarationCache.getEntry, {
179− workspaceId,
180− cacheKey: "cache:3",
181− maxAgeMs: 10_000,
182− });
183−
184− expect(oldest).toBeNull();
185− expect(newestA).not.toBeNull();
186− expect(newestB).not.toBeNull();
187− });
188−});
executor/packages/convex/typecheckDeclarationCache.ts+0−76
@@ -1,76 +0,0 @@
1−import { v } from "convex/values";
2−import { internalMutation, internalQuery } from "./_generated/server";
3−
4−export const getEntry = internalQuery({
5− args: {
6− workspaceId: v.id("workspaces"),
7− cacheKey: v.string(),
8− maxAgeMs: v.number(),
9− },
10− handler: async (ctx, args) => {
11− const entry = await ctx.db
12− .query("typecheckDeclarationCache")
13− .withIndex("by_workspace_cache_key", (q) =>
14− q.eq("workspaceId", args.workspaceId).eq("cacheKey", args.cacheKey),
15− )
16− .unique();
17−
18− if (!entry) return null;
19−
20− const ageMs = Date.now() - entry.createdAt;
21− if (ageMs > args.maxAgeMs) return null;
22−
23− return {
24− storageId: entry.storageId,
25− sizeBytes: entry.sizeBytes,
26− createdAt: entry.createdAt,
27− };
28− },
29−});
30−
31−export const putEntry = internalMutation({
32− args: {
33− workspaceId: v.id("workspaces"),
34− cacheKey: v.string(),
35− storageId: v.id("_storage"),
36− sizeBytes: v.number(),
37− maxEntriesPerWorkspace: v.optional(v.number()),
38− },
39− handler: async (ctx, args) => {
40− const existing = await ctx.db
41− .query("typecheckDeclarationCache")
42− .withIndex("by_workspace_cache_key", (q) =>
43− q.eq("workspaceId", args.workspaceId).eq("cacheKey", args.cacheKey),
44− )
45− .unique();
46−
47− if (existing) {
48− await ctx.storage.delete(existing.storageId).catch(() => {});
49− await ctx.db.delete(existing._id);
50− }
51−
52− await ctx.db.insert("typecheckDeclarationCache", {
53− workspaceId: args.workspaceId,
54− cacheKey: args.cacheKey,
55− storageId: args.storageId,
56− sizeBytes: args.sizeBytes,
57− createdAt: Date.now(),
58− });
59−
60− const maxEntries = Math.max(1, Math.floor(args.maxEntriesPerWorkspace ?? 64));
61− const entries = await ctx.db
62− .query("typecheckDeclarationCache")
63− .withIndex("by_workspace_created", (q) => q.eq("workspaceId", args.workspaceId))
64− .order("desc")
65− .collect();
66−
67− if (entries.length <= maxEntries) {
68− return;
69− }
70−
71− for (const stale of entries.slice(maxEntries)) {
72− await ctx.storage.delete(stale.storageId).catch(() => {});
73− await ctx.db.delete(stale._id);
74− }
75− },
76−});
executor/packages/core/src/mcp-server.test.ts+6−48
@@ -10,11 +10,6 @@ class FakeMcpService { · 9 unmodified lines
1010 private readonly tasks = new Map<string, TaskRecord>();
1111 private readonly sessions = new Map<string, AnonymousContext>();
1212 private readonly listeners = new Map<string, Set<(event: LiveTaskEvent) => void>>();
13− private typecheckPayload?: { tools: ToolDescriptor[]; dtsUrls: Record<string, string> };
14−
15− setTypecheckPayload(payload: { tools: ToolDescriptor[]; dtsUrls: Record<string, string> }) {
16− this.typecheckPayload = payload;
17− }
1813
1914 async createTask(input: CreateTaskInput): Promise<{ task: TaskRecord }> {
2015 const id = `task_${crypto.randomUUID()}`;
@@ -99,18 +94,6 @@ class FakeMcpService { · 78 unmodified lines
9994 { path: "utils.get_time", description: "Get the current time", approval: "auto" },
10095 ];
10196 }
102−
103− async listToolsForTypecheck(
104− _context: { workspaceId: string; actorId?: string; clientId?: string },
105− ): Promise<{ tools: ToolDescriptor[]; dtsUrls: Record<string, string> }> {
106− if (this.typecheckPayload) {
107− return this.typecheckPayload;
108− }
109− return {
110− tools: await this.listTools(_context),
111− dtsUrls: {},
112− };
113− }
11497 }
11598
11699 async function withMcpClient<T>(
@@ -219,37 +202,14 @@ test("run_code MCP tool bootstraps anonymous context when workspace is omitted", · 102 unmodified lines
219202 });
220203 });
221204
222−test("run_code typecheck uses OpenAPI .d.ts from typecheck context", async () => {
205+test("run_code MCP tool does not gate on TypeScript errors", async () => {
223206 const service = new FakeMcpService();
224− service.setTypecheckPayload({
225− tools: [
226− {
227− path: "github.issues.list_for_repo",
228− description: "List issues for a repository",
229− approval: "auto",
230− source: "openapi:github",
231− operationId: "issues/list-for-repo",
232− },
233− ],
234− dtsUrls: {
235− "openapi:github":
236− "data:text/plain,"
237− + encodeURIComponent(
238− `export interface operations {
239− "issues/list-for-repo": {
240− parameters: { path: { owner: string; repo: string } };
241− responses: { 200: { content: { "application/json": { id: number }[] } } };
242− };
243−}`,
244− ),
245− },
246− });
247207
248208 await withMcpClient(service, async (client) => {
249209 const result = (await client.callTool({
250210 name: "run_code",
251211 arguments: {
252− code: "await tools.github.issues.list_for_repo({ owner: 123, repo: 'answeroverflow' });",
212+ code: "const value: string = 123; return value;",
253213 },
254214 })) as {
255215 isError?: boolean;
@@ -257,11 +217,9 @@ test("run_code typecheck uses OpenAPI .d.ts from typecheck context", async () => · 1 unmodified line
257217 structuredContent?: Record<string, unknown>;
258218 };
259219
260− expect(result.isError).toBe(true);
261− const typecheckErrors = Array.isArray(result.structuredContent?.typecheckErrors)
262− ? (result.structuredContent!.typecheckErrors as string[])
263− : [];
264− expect(typecheckErrors.length).toBeGreaterThan(0);
265− expect(typecheckErrors.some((error) => error.includes("owner") || error.includes("number"))).toBe(true);
220+ expect(result.isError).toBeUndefined();
221+ const text = result.content.find((part) => part.type === "text");
222+ expect(text?.text).toContain("status: completed");
223+ expect(result.structuredContent?.typecheckErrors).toBeUndefined();
266224 });
267225 });
executor/packages/core/src/mcp-server.ts+3−100
@@ -2,8 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; · 1 unmodified line
22 import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
33 import { Result } from "better-result";
44 import { z } from "zod";
5−import { loadSourceDtsByUrl } from "./dts-loader";
6−import { generateToolDeclarations, generateToolInventory, typecheckCode } from "./typechecker";
5+import { generateToolInventory } from "./typechecker";
76 import type { LiveTaskEvent } from "./events";
87 import type {
98 AnonymousContext,
@@ -29,15 +28,6 @@ interface McpExecutorService { · 19 unmodified lines
2928 subscribe(taskId: string, workspaceId: Id<"workspaces">, listener: (event: LiveTaskEvent) => void): () => void;
3029 bootstrapAnonymousContext(sessionId?: string): Promise<AnonymousContext>;
3130 listTools(context?: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string }): Promise<ToolDescriptor[]>;
32− listToolsForTypecheck?(
33− context: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string },
34− ): Promise<{ tools: ToolDescriptor[]; dtsUrls: Record<string, string> }>;
35− typecheckRunCode?(input: {
36− code: string;
37− workspaceId: Id<"workspaces">;
38− actorId?: string;
39− clientId?: string;
40− }): Promise<{ ok: boolean; errors: string[]; tools: ToolDescriptor[] }>;
4131 listPendingApprovals?(workspaceId: Id<"workspaces">): Promise<PendingApprovalRecord[]>;
4232 resolveApproval?(input: {
4333 workspaceId: Id<"workspaces">;
@@ -238,7 +228,7 @@ function waitForTerminalTask( · 194 unmodified lines
238228
239229 function buildRunCodeDescription(tools?: ToolDescriptor[]): string {
240230 const base =
241− "Execute TypeScript code in a sandboxed runtime. The code has access to a `tools` object with typed methods for calling external services. Use `return` to return a value. Waits for completion and returns only explicit return values (console output is not returned). Code is typechecked before execution — type errors are returned without running. Runtime has no filesystem/process/import access; use `tools.*` for external calls.";
231+ "Execute TypeScript code in a sandboxed runtime. The code has access to a `tools` object with typed methods for calling external services. Use `return` to return a value. Waits for completion and returns only explicit return values (console output is not returned). Runtime has no filesystem/process/import access; use `tools.*` for external calls.";
242232 const toolList = tools ?? [];
243233 const topLevelKeys = listTopLevelToolKeys(toolList);
244234 const rootKeysNote = topLevelKeys.length > 0
@@ -381,83 +371,6 @@ function createRunCodeTool( · 136 unmodified lines
381371 };
382372 }
383373
384− // Typecheck code before execution — align with Monaco by loading source .d.ts
385− // for OpenAPI tools when available.
386− let toolsForContext: ToolDescriptor[];
387− let typecheck: { ok: boolean; errors: readonly string[] };
388− if (service.typecheckRunCode) {
389− const result = await service.typecheckRunCode({
390− code: input.code,
391− workspaceId: context.workspaceId,
392− actorId: context.actorId,
393− clientId: context.clientId,
394− });
395− toolsForContext = result.tools;
396− typecheck = {
397− ok: result.ok,
398− errors: result.errors,
399− };
400− } else {
401− let sourceDtsBySource: Record<string, string> = {};
402− if (service.listToolsForTypecheck) {
403− const { tools, dtsUrls } = await service.listToolsForTypecheck({
404− workspaceId: context.workspaceId,
405− actorId: context.actorId,
406− clientId: context.clientId,
407− });
408− toolsForContext = tools;
409− sourceDtsBySource = await loadSourceDtsByUrl(dtsUrls ?? {});
410− } else {
411− toolsForContext = await service.listTools({
412− workspaceId: context.workspaceId,
413− actorId: context.actorId,
414− clientId: context.clientId,
415− });
416− }
417−
418− const declarations = generateToolDeclarations(toolsForContext, {
419− sourceDtsBySource,
420− });
421− typecheck = typecheckCode(input.code, declarations);
422− }
423−
424− if (!typecheck.ok) {
425− const topLevelKeys = listTopLevelToolKeys(toolsForContext);
426− const toolsShadowingError = typecheck.errors.some((error) =>
427− error.includes("Block-scoped variable 'tools' used before its declaration")
428− || error.includes("'tools' implicitly has type 'any'"),
429− );
430−
431− const hintLines: string[] = [];
432− if (toolsShadowingError) {
433− hintLines.push("Hint: avoid declaring a local variable named `tools`.");
434− hintLines.push("Example: `const discovered = await tools.discover({ query: \"github issues\" });`");
435− }
436− if (topLevelKeys.length > 0) {
437− hintLines.push(`Available top-level tool keys: ${topLevelKeys.join(", ")}`);
438− }
439−
440− const errorText = [
441− "TypeScript type errors in generated code:",
442− "",
443− ...typecheck.errors.map((e) => ` ${e}`),
444− ...(hintLines.length > 0 ? ["", ...hintLines] : []),
445− "",
446− "Fix the type errors and try again.",
447− ].join("\n");
448−
449− return {
450− content: [textContent(errorText)],
451− isError: true,
452− structuredContent: {
453− typecheckErrors: typecheck.errors,
454− workspaceId: context.workspaceId,
455− actorId: context.actorId,
456− sessionId: context.sessionId,
457− },
458− };
459− }
460−
461374 const created = await service.createTask({
462375 code: input.code,
463376 timeoutMs: requestedTimeoutMs,
@@ -589,20 +502,10 @@ async function createMcpServer( · 125 unmodified lines
589502 );
590503 const onApprovalPrompt = createMcpApprovalPrompt(mcp);
591504
592− // If workspace context is provided, fetch tool inventory for richer description
593− let tools: ToolDescriptor[] | undefined;
594− if (context) {
595− tools = await service.listTools({
596− workspaceId: context.workspaceId,
597− actorId: context.actorId,
598− clientId: context.clientId,
599− });
600− }
601−
602505 mcp.registerTool(
603506 "run_code",
604507 {
605− description: buildRunCodeDescription(tools),
508+ description: buildRunCodeDescription(),
606509 inputSchema: context ? BOUND_INPUT : FULL_INPUT,
607510 },
608511 createRunCodeTool(service, context, onApprovalPrompt),
kill-all.ts+3−2
@@ -2,7 +2,7 @@ · 1 unmodified line
22 * Kills all processes started by `bun dev`.
33 *
44 * 1. Reads PIDs from .dev.pids and kills process trees
5− * 2. Falls back to killing anything on known dev ports (3000, 3002)
5+ * 2. Falls back to killing anything on known dev ports (assistant port)
66 *
77 * Usage: bun run kill:all
88 */
@@ -11,7 +11,8 @@ import { join } from "node:path"; · 2 unmodified lines
1111 import { unlinkSync } from "node:fs";
1212
1313 const PID_FILE = join(import.meta.dir, ".dev.pids");
14−const DEV_PORTS = [3000, 3002];
14+const ASSISTANT_PORT = Number(Bun.env.ASSISTANT_PORT ?? 3002);
15+const DEV_PORTS = [ASSISTANT_PORT];
1516
1617 let killed = 0;
1718 let skipped = 0;