Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e1c98dc
Add organization billing model and members/billing console flows
RhysSullivan Feb 8, 2026
464c53b
Refactor org auth guards with convex custom functions
RhysSullivan Feb 8, 2026
eea4972
Use custom auth mutation wrapper for workspace creation
RhysSullivan Feb 8, 2026
8e23be5
Hide direct account guard behind Convex wrappers
RhysSullivan Feb 8, 2026
ffb327f
Polish web workspace shell and member billing flows
RhysSullivan Feb 8, 2026
ddca61b
Disable invite creation when WorkOS is not enabled
RhysSullivan Feb 8, 2026
32d4773
Wire WorkOS invitation delivery and acceptance sync
RhysSullivan Feb 8, 2026
db590c6
Allow revoking pending invites from members UI
RhysSullivan Feb 8, 2026
5919843
Auto-link organizations to WorkOS when sending invites
RhysSullivan Feb 8, 2026
cb5ab27
Move WorkOS invite IO into actions using SDK
RhysSullivan Feb 8, 2026
60728c0
Preserve Convex ID types in org and invite responses
RhysSullivan Feb 8, 2026
ed3e4e3
Refactor executor integration to use Convex-native methods, removing …
RhysSullivan Feb 8, 2026
ea91ac8
Extract reusable slug collision retry helper
RhysSullivan Feb 8, 2026
023aa96
Tighten auth helper typing and remove db any usage
RhysSullivan Feb 8, 2026
78cbbc3
Remove helper any usage in auth organization sync
RhysSullivan Feb 8, 2026
356f9f6
Require organization IDs on all workspaces
RhysSullivan Feb 8, 2026
164f182
Remove workspace kind and require organization ownership
RhysSullivan Feb 8, 2026
859e5ba
Drop legacy workspace IDs and use workspace doc ids
RhysSullivan Feb 8, 2026
d62bd7a
Remove workspace visibility from data model
RhysSullivan Feb 8, 2026
f0d8001
Remove accountSessions and rely on WorkOS + anonymous sessions
RhysSullivan Feb 8, 2026
d03cf18
Unify workspace membership model and tighten core types
RhysSullivan Feb 8, 2026
7e37560
Consolidate account and workspace APIs into app/workspaces modules
RhysSullivan Feb 8, 2026
7e076dd
Simplify invite flow and tighten operational schema enums
RhysSullivan Feb 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions assistant/packages/bot/src/commands/ask.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import type { ChatInputCommandInteraction, CommandInteraction } from "discord.js";
import type { Client } from "@assistant/server/client";
import { unwrap } from "@assistant/server/client";
import type { ExecutorClient } from "@assistant/server/executor-client";
import type { ConvexReactClient } from "convex/react";
import { ConvexProvider } from "convex/react";
import type { ReacordInstance } from "@openassistant/reacord";
Expand All @@ -14,7 +13,6 @@ import { TaskMessage } from "../views/task-message";

interface AskCommandDeps {
readonly api: Client;
readonly executor: ExecutorClient;
readonly convex: ConvexReactClient;
readonly reacord: {
reply: (interaction: CommandInteraction, content: React.ReactNode) => Effect.Effect<ReacordInstance>;
Expand Down Expand Up @@ -53,7 +51,6 @@ export async function handleAskCommand(
agentTaskId={agentTaskId}
prompt={prompt}
workspaceId={workspaceId}
executor={deps.executor}
/>
</ConvexProvider>,
),
Expand Down
6 changes: 1 addition & 5 deletions assistant/packages/bot/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
} from "discord.js";
import { makeReacord } from "@openassistant/reacord";
import { createClient } from "@assistant/server/client";
import { createExecutorClient } from "@assistant/server/executor-client";
import { ConvexReactClient } from "convex/react";
import { handleAskCommand } from "./commands/ask";

Expand All @@ -28,7 +27,6 @@ if (!DISCORD_TOKEN) {
}

const SERVER_URL = Bun.env.ASSISTANT_SERVER_URL ?? "http://localhost:3000";
const EXECUTOR_URL = Bun.env.EXECUTOR_URL ?? "http://localhost:4001";
const CONVEX_URL = Bun.env.CONVEX_URL ?? "http://127.0.0.1:3210";

// ---------------------------------------------------------------------------
Expand All @@ -44,7 +42,6 @@ const client = new Client({

const reacord = makeReacord(client, { maxInstances: 50 });
const api = createClient(SERVER_URL);
const executor = createExecutorClient(EXECUTOR_URL);
const convex = new ConvexReactClient(CONVEX_URL, {
unsavedChangesWarning: false,
});
Expand Down Expand Up @@ -87,7 +84,7 @@ client.on("interactionCreate", async (interaction) => {

switch (interaction.commandName) {
case "ask":
await handleAskCommand(interaction, { api, executor, convex, reacord });
await handleAskCommand(interaction, { api, convex, reacord });
break;
case "clear":
await interaction.reply({ content: `_${"\n".repeat(50)}_` });
Expand All @@ -105,7 +102,6 @@ client.once("clientReady", async () => {
console.log(`Logged in as ${client.user?.tag}`);
await registerCommands();
console.log(`Connected to server at ${SERVER_URL}`);
console.log(`Executor at ${EXECUTOR_URL}`);
console.log(`Convex at ${CONVEX_URL}`);
});

Expand Down
14 changes: 7 additions & 7 deletions assistant/packages/bot/src/views/task-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* - Watches agentTask in Convex for status, result, error (reactive, no polling)
* - Watches pending approvals in Convex (reactive)
* - Approval buttons resolve via executor REST API
* - Approval buttons resolve via Convex mutation
*/

import { useState, useCallback } from "react";
Expand All @@ -16,9 +16,8 @@ import {
Loading,
useInstance,
} from "@openassistant/reacord";
import { useQuery } from "convex/react";
import { useMutation, useQuery } from "convex/react";
import { api } from "@executor/convex/_generated/api";
import type { ExecutorClient } from "@assistant/server/executor-client";

// ---------------------------------------------------------------------------
// Component
Expand All @@ -28,11 +27,11 @@ export interface TaskMessageProps {
readonly agentTaskId: string;
readonly prompt: string;
readonly workspaceId: string;
readonly executor: ExecutorClient;
}

export function TaskMessage({ agentTaskId, prompt, workspaceId, executor }: TaskMessageProps) {
export function TaskMessage({ agentTaskId, prompt, workspaceId }: TaskMessageProps) {
const instance = useInstance();
const resolveApproval = useMutation(api.executor.resolveApproval);

// Reactive queries — no polling!
const agentTask = useQuery(api.database.getAgentTask, { agentTaskId });
Expand All @@ -48,14 +47,15 @@ export function TaskMessage({ agentTaskId, prompt, workspaceId, executor }: Task

const handleApproval = useCallback(async (approvalId: string, decision: "approved" | "denied") => {
try {
await executor.api.approvals({ approvalId }).post({
await resolveApproval({
workspaceId,
approvalId,
decision,
});
} catch (err) {
console.error(`[approval ${approvalId}]`, err);
}
}, [executor, workspaceId]);
}, [resolveApproval, workspaceId]);

const accentColor = isDone
? status === "completed" ? 0x57f287 : 0xed4245
Expand Down
3 changes: 1 addition & 2 deletions assistant/packages/bot/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
"esModuleInterop": true,
"skipLibCheck": true,
"paths": {
"@executor/convex/*": ["../../../executor/convex/*"],
"@executor/server/*": ["../../../executor/apps/server/*"]
"@executor/convex/*": ["../../../executor/convex/*"]
}
},
"include": ["src"]
Expand Down
38 changes: 33 additions & 5 deletions assistant/packages/core/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,23 @@ interface McpTool {
inputSchema?: Record<string, unknown>;
}

async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
const timeoutPromise = new Promise<never>((_resolve, reject) => {
timeoutHandle = setTimeout(() => {
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
}, timeoutMs);
});

try {
return await Promise.race([promise, timeoutPromise]);
} finally {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
}
}

export interface AgentOptions {
readonly executorUrl: string;
readonly workspaceId: string;
Expand Down Expand Up @@ -143,6 +160,12 @@ export function createAgent(options: AgentOptions) {

const resolvedApiKey = resolveApiKey(apiKey);
const model = getModel("anthropic", modelId as never);
const executorBaseUrl = (() => {
const raw = executorUrl.trim().replace(/\/$/, "");
if (/^https?:\/\//.test(raw)) return raw;
if (raw.includes(".convex.cloud")) return raw.replace(".convex.cloud", ".convex.site");
throw new Error(`Invalid executorUrl: expected http(s) URL, got '${executorUrl}'`);
})();

async function generate(messages: Message[], tools: McpTool[]) {
const { systemPrompt, piMessages } = toPiMessages(messages);
Expand All @@ -167,7 +190,7 @@ export function createAgent(options: AgentOptions) {

// Connect to executor MCP
emit({ type: "status", message: "Connecting..." });
const mcpUrl = new URL(`${executorUrl}/mcp`);
const mcpUrl = new URL(`${executorBaseUrl}/mcp`);
mcpUrl.searchParams.set("workspaceId", workspaceId);
mcpUrl.searchParams.set("actorId", actorId);
if (clientId) mcpUrl.searchParams.set("clientId", clientId);
Expand All @@ -177,7 +200,7 @@ export function createAgent(options: AgentOptions) {

let mcpConnected = false;
try {
await mcp.connect(transport);
await withTimeout(mcp.connect(transport), 20_000, "MCP connection");
mcpConnected = true;
} catch (err) {
const msg = `Failed to connect to executor MCP: ${err instanceof Error ? err.message : String(err)}`;
Expand All @@ -189,7 +212,7 @@ export function createAgent(options: AgentOptions) {
try {
// List tools
emit({ type: "status", message: "Loading tools..." });
const { tools: rawTools } = await mcp.listTools();
const { tools: rawTools } = await withTimeout(mcp.listTools(), 20_000, "MCP listTools");
const tools: McpTool[] = rawTools.map((t) => ({
name: t.name,
description: t.description,
Expand Down Expand Up @@ -219,6 +242,7 @@ ${toolSection}

- Use the \`run_code\` tool to execute TypeScript code
- Write complete, self-contained scripts — do all work in a single run_code call when possible
- TypeScript syntax is allowed; prefer simple runnable scripts over heavy type scaffolding
- The code runs in a sandbox — only \`tools.*\` calls are available (no fetch, require, import)
- Handle errors with try/catch
- Return a structured result, then summarize what happened
Expand All @@ -235,7 +259,7 @@ ${toolSection}

// Agent loop
while (toolCallCount < maxToolCalls) {
const response = await generate(messages, tools);
const response = await withTimeout(generate(messages, tools), 90_000, "Model response");

if (!response.toolCalls?.length) {
const text = response.text ?? "";
Expand All @@ -253,7 +277,11 @@ ${toolSection}
emit({ type: "status", message: `Running ${tc.name}...` });

// Call tool via MCP
const result = await mcp.callTool({ name: tc.name, arguments: tc.args });
const result = await withTimeout(
mcp.callTool({ name: tc.name, arguments: tc.args }),
120_000,
`MCP tool call (${tc.name})`,
);
const text = (result.content as Array<{ type: string; text?: string }>)
.filter((c) => c.type === "text" && c.text)
.map((c) => c.text!)
Expand Down
3 changes: 1 addition & 2 deletions assistant/packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
"type": "module",
"exports": {
".": "./src/index.ts",
"./client": "./src/client.ts",
"./executor-client": "./src/executor-client.ts"
"./client": "./src/client.ts"
},
"scripts": {
"dev": "bun --hot src/index.ts",
Expand Down
12 changes: 0 additions & 12 deletions assistant/packages/server/src/executor-client.ts

This file was deleted.

46 changes: 38 additions & 8 deletions assistant/packages/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,51 @@
*/

import { createApp } from "./routes";
import { createExecutorClient } from "./executor-client";
import { ConvexHttpClient } from "convex/browser";
import { api } from "@executor/convex/_generated/api";

const PORT = Number(Bun.env.PORT ?? 3000);
const EXECUTOR_URL = Bun.env.EXECUTOR_URL ?? "http://localhost:4001";
const CONVEX_URL = Bun.env.CONVEX_URL ?? "http://127.0.0.1:3210";
const EXECUTOR_URL = Bun.env.EXECUTOR_URL
?? Bun.env.CONVEX_SITE_URL
?? (CONVEX_URL.includes(".convex.cloud")
? CONVEX_URL.replace(".convex.cloud", ".convex.site")
: CONVEX_URL);
const PINNED_WORKSPACE_ID = Bun.env.EXECUTOR_WORKSPACE_ID?.trim();
const PINNED_ACTOR_ID = Bun.env.EXECUTOR_ACTOR_ID?.trim();
const PINNED_CLIENT_ID = Bun.env.EXECUTOR_CLIENT_ID?.trim();
const ANON_SESSION_ID = Bun.env.EXECUTOR_ANON_SESSION_ID?.trim();

// Bootstrap anonymous context on executor
const executor = createExecutorClient(EXECUTOR_URL);
const { data: ctx, error: bootstrapError } = await executor.api.auth.anonymous.bootstrap.post({});
// Bootstrap anonymous context on executor Convex backend
const convex = new ConvexHttpClient(CONVEX_URL);
let ctx: { workspaceId: string; actorId: string; clientId?: string };

if (bootstrapError || !ctx) {
console.error("Failed to bootstrap executor context. Is the executor running at", EXECUTOR_URL, "?");
process.exit(1);
if (PINNED_WORKSPACE_ID && PINNED_ACTOR_ID) {
ctx = {
workspaceId: PINNED_WORKSPACE_ID,
actorId: PINNED_ACTOR_ID,
clientId: PINNED_CLIENT_ID,
};
} else {
try {
const bootstrap = await convex.mutation(api.database.bootstrapAnonymousSession, {
sessionId: ANON_SESSION_ID,
});
ctx = {
workspaceId: bootstrap.workspaceId,
actorId: bootstrap.actorId,
clientId: PINNED_CLIENT_ID ?? bootstrap.clientId,
};
} catch (error) {
console.error("Failed to bootstrap executor context from Convex at", CONVEX_URL, error);
process.exit(1);
}
}

console.log(`[assistant] executor context: workspace=${ctx.workspaceId} actor=${ctx.actorId}`);
if (ANON_SESSION_ID) {
console.log(`[assistant] executor anon session: ${ANON_SESSION_ID}`);
}

const contextLines: string[] = [];
if (Bun.env.POSTHOG_PROJECT_ID) contextLines.push(`- PostHog project ID: ${Bun.env.POSTHOG_PROJECT_ID}`);
Expand Down
3 changes: 1 addition & 2 deletions assistant/packages/server/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
"esModuleInterop": true,
"skipLibCheck": true,
"paths": {
"@executor/convex/*": ["../../../executor/convex/*"],
"@executor/server/*": ["../../../executor/apps/server/*"]
"@executor/convex/*": ["../../../executor/convex/*"]
}
},
"include": ["src"]
Expand Down
Loading