diff --git a/discord-read/server/lib/trigger-store.ts b/discord-read/server/lib/trigger-store.ts index cf90f545..b3695bf8 100644 --- a/discord-read/server/lib/trigger-store.ts +++ b/discord-read/server/lib/trigger-store.ts @@ -42,12 +42,20 @@ class LazyStudioKV implements TriggerStorage { } async get(connectionId: string) { - return this.inner?.get(connectionId) ?? null; + const result = (await this.inner?.get(connectionId)) ?? null; + console.log( + `[TriggerStorage] GET ${connectionId}: ${result ? "found credentials" : "empty"} (ready=${this.isReady})`, + ); + return result; } async set(connectionId: string, state: any) { + console.log( + `[TriggerStorage] SET ${connectionId}: saving credentials (ready=${this.isReady})`, + ); await this.inner?.set(connectionId, state); } async delete(connectionId: string) { + console.log(`[TriggerStorage] DELETE ${connectionId}`); await this.inner?.delete(connectionId); } } diff --git a/discord-read/server/main.ts b/discord-read/server/main.ts index 3d3cc92d..284a8bbc 100644 --- a/discord-read/server/main.ts +++ b/discord-read/server/main.ts @@ -132,6 +132,9 @@ const runtime = withRuntime({ } // If we have a connection ID, sync to config-cache + console.log( + `[CONFIG] Save check: connectionId=${connectionId || "MISSING"}, organizationId=${organizationId || "MISSING"}, meshUrl=${meshUrl ? "yes" : "MISSING"}, authorization=${authorization ? "yes" : "MISSING"}`, + ); if (connectionId && organizationId && meshUrl) { const existingConfig = await getDiscordConfig(connectionId); @@ -169,6 +172,13 @@ const runtime = withRuntime({ console.log( `[CONFIG] Authorized Guilds: ${authorizedGuilds.length > 0 ? authorizedGuilds.join(", ") : "all"}`, ); + console.log( + `[CONFIG] Bot token: ${botToken ? `${botToken.slice(0, 10)}...${botToken.slice(-4)}` : "MISSING"}`, + ); + } else { + console.warn( + `[CONFIG] ⚠️ Cannot save config — missing: ${!connectionId ? "connectionId " : ""}${!organizationId ? "organizationId " : ""}${!meshUrl ? "meshUrl" : ""}`, + ); } // Auto-initialize Discord client for this connection @@ -308,6 +318,11 @@ async function bootstrapFromSupabase(): Promise { console.log(`[BOOTSTRAP] Found ${rows.length} saved connection(s)`); + // Deduplicate by bot_token — only start one Discord client per unique token. + // Multiple connections may share the same bot; we pick the first and register + // the rest as aliases so config-cache / triggers still work for them. + const startedTokens = new Set(); + for (const row of rows) { const connectionId = row.connection_id; @@ -325,19 +340,7 @@ async function bootstrapFromSupabase(): Promise { triggerStorage.configure(meshUrl, meshApiKey); } - // Build a synthetic env so ensureBotRunning can resolve the token - const syntheticEnv = { - MESH_REQUEST_CONTEXT: { - connectionId, - organizationId: row.organization_id, - meshUrl, - token: meshApiKey || undefined, - authorization: `Bearer ${row.bot_token}`, - state: {}, - }, - } as unknown as Env; - - // Sync config to in-memory cache + // Sync config to in-memory cache (always, even for duplicate tokens) const { setDiscordConfig } = await import("./lib/config-cache.ts"); await setDiscordConfig({ connectionId, @@ -353,6 +356,26 @@ async function bootstrapFromSupabase(): Promise { commandPrefix: row.command_prefix || "!", }); + // Skip starting a second Discord client for the same bot token + if (startedTokens.has(row.bot_token)) { + console.log( + `[BOOTSTRAP] Skipping ${connectionId} — bot already started for this token`, + ); + continue; + } + + // Build a synthetic env so ensureBotRunning can resolve the token + const syntheticEnv = { + MESH_REQUEST_CONTEXT: { + connectionId, + organizationId: row.organization_id, + meshUrl, + token: meshApiKey || undefined, + authorization: `Bearer ${row.bot_token}`, + state: {}, + }, + } as unknown as Env; + // Ensure instance is created (superAdmins come from StateSchema on next onChange) getOrCreateInstance(connectionId, syntheticEnv); @@ -360,6 +383,7 @@ async function bootstrapFromSupabase(): Promise { console.log(`[BOOTSTRAP] Starting bot for ${connectionId}...`); const started = await ensureBotRunning(syntheticEnv); if (started) { + startedTokens.add(row.bot_token); console.log(`[BOOTSTRAP] Bot started for ${connectionId} ✓`); } else { console.log(`[BOOTSTRAP] Bot failed to start for ${connectionId}`); diff --git a/discord-read/server/tools/config.ts b/discord-read/server/tools/config.ts index 1ff578be..8a91a3af 100644 --- a/discord-read/server/tools/config.ts +++ b/discord-read/server/tools/config.ts @@ -104,10 +104,14 @@ export const createSaveConfigTool = (env: Env) => // Get connection ID from env const connectionId = - env.MESH_REQUEST_CONTEXT?.connectionId || "default-connection"; + env?.MESH_REQUEST_CONTEXT?.connectionId || "default-connection"; const organizationId = - env.MESH_REQUEST_CONTEXT?.organizationId || "default-org"; - const meshUrl = env.MESH_REQUEST_CONTEXT?.meshUrl || ""; + env?.MESH_REQUEST_CONTEXT?.organizationId || "default-org"; + const meshUrl = env?.MESH_REQUEST_CONTEXT?.meshUrl || ""; + + console.log( + `[Tool] DISCORD_SAVE_CONFIG: connectionId=${connectionId}, organizationId=${organizationId}, meshUrl=${meshUrl ? "yes" : "EMPTY"}, env=${!!env}, runtimeContext=${!!runtimeContext}`, + ); // Create config object const config: DiscordConfig = {