From 17dfaa00cce7c2b0f09aff9dd115ab79303546ee Mon Sep 17 00:00:00 2001 From: Jonas Jesus Date: Tue, 7 Apr 2026 14:42:00 -0300 Subject: [PATCH 1/3] fix(discord): deduplicate bootstrap by bot token Multiple connections in Supabase can share the same bot token (duplicate entries). Without dedup, the bootstrap started multiple Discord.js clients with the same token, causing conflicts and duplicate events. Now tracks started tokens and skips connections that share an already running bot. Config cache is still synced for all connections. Co-Authored-By: Claude Opus 4.6 --- discord-read/server/main.ts | 40 +++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/discord-read/server/main.ts b/discord-read/server/main.ts index 3d3cc92d..6e8678ed 100644 --- a/discord-read/server/main.ts +++ b/discord-read/server/main.ts @@ -308,6 +308,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 +330,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 +346,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 +373,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}`); From 43445d4dd65ee1ca2a788b0203c5fb8ffdf76711 Mon Sep 17 00:00:00 2001 From: Jonas Jesus Date: Tue, 7 Apr 2026 14:50:45 -0300 Subject: [PATCH 2/3] chore(discord): add diagnostic logs for config save conditions Logs which fields are present/missing when onChange tries to save config to Supabase, and when DISCORD_SAVE_CONFIG tool is called. This helps debug why some connections (e.g. deco-help) fail to persist. Co-Authored-By: Claude Opus 4.6 --- discord-read/server/main.ts | 10 ++++++++++ discord-read/server/tools/config.ts | 10 +++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/discord-read/server/main.ts b/discord-read/server/main.ts index 6e8678ed..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 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 = { From 55d71afd4644729b0ad046ed8488bf5f050564de Mon Sep 17 00:00:00 2001 From: Jonas Jesus Date: Tue, 7 Apr 2026 14:51:35 -0300 Subject: [PATCH 3/3] chore(discord): add diagnostic logs to trigger storage get/set/delete Logs every read/write to LazyStudioKV so we can see if TRIGGER_CONFIGURE is saving credentials and if notify() is finding them on reload. Co-Authored-By: Claude Opus 4.6 --- discord-read/server/lib/trigger-store.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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); } }