From 9d648862590b40386f73d4ac8187ab7e810a05e2 Mon Sep 17 00:00:00 2001 From: matamune Date: Thu, 14 May 2026 14:48:32 +0000 Subject: [PATCH] Refresh stale Discord gateway sessions --- apps/discord-bridge/src/bridge.ts | 23 ++++++++-- apps/discord-bridge/src/state.ts | 1 + apps/discord-bridge/src/types.ts | 1 + apps/discord-bridge/test/bridge.test.ts | 59 +++++++++++++++++++++++++ apps/discord-bridge/test/state.test.ts | 2 + 5 files changed, 82 insertions(+), 4 deletions(-) diff --git a/apps/discord-bridge/src/bridge.ts b/apps/discord-bridge/src/bridge.ts index 6d4e78f..9318284 100644 --- a/apps/discord-bridge/src/bridge.ts +++ b/apps/discord-bridge/src/bridge.ts @@ -28,6 +28,7 @@ import type { } from "./types.ts"; const maxDiscordMessageLength = 2000; +const gatewayToolsVersion = 1; type ThreadSnapshot = { terminalTurnIds: string[]; @@ -572,7 +573,7 @@ export class DiscordCodexBridge { `Delegations: ${delegations.length} tracked, ${activeDelegations.length} active`, "", "**Delegation Backend**", - `Status: ${session ? "privileged gateway tools available to the main Codex operator thread" : "waiting for main Codex operator thread"}.`, + `Status: ${state.gateway?.toolsVersion === gatewayToolsVersion ? "privileged gateway tools available to the main Codex operator thread" : "waiting for a tool-enabled main Codex operator thread"}.`, `Flow backend: \`${this.config.flowBackendUrl ?? "not configured"}\``, "", "**Detail Threads**", @@ -701,22 +702,33 @@ export class DiscordCodexBridge { } const state = this.#requireState(); const existing = this.#gatewaySession(); - if (existing) { + const shouldReuseExisting = + Boolean(gatewayConfig.mainThreadId) || + state.gateway?.toolsVersion === gatewayToolsVersion; + if (existing && shouldReuseExisting) { state.gateway = { homeChannelId: gatewayConfig.homeChannelId, mainThreadId: existing.codexThreadId, statusMessageId: existing.statusMessageId, createdAt: existing.createdAt, + toolsVersion: state.gateway?.toolsVersion, delegations: state.gateway?.delegations ?? [], }; this.#registerRunner(existing); await this.#persist(); return; } + if (existing) { + state.sessions = state.sessions.filter((session) => session !== existing); + this.#runnersByDiscordThread.delete(existing.discordThreadId); + this.#runnersByCodexThread.delete(existing.codexThreadId); + } const configuredThreadId = - state.gateway?.mainThreadId ?? - gatewayConfig.mainThreadId; + gatewayConfig.mainThreadId ?? + (state.gateway?.toolsVersion === gatewayToolsVersion + ? state.gateway.mainThreadId + : undefined); const title = "Codex Gateway"; const started = configuredThreadId ? await this.client.resumeThread(this.#threadResumeParams( @@ -747,6 +759,9 @@ export class DiscordCodexBridge { homeChannelId: gatewayConfig.homeChannelId, mainThreadId: codexThreadId, createdAt: session.createdAt, + toolsVersion: configuredThreadId + ? state.gateway?.toolsVersion + : gatewayToolsVersion, delegations: state.gateway?.delegations ?? [], }; state.sessions.push(session); diff --git a/apps/discord-bridge/src/state.ts b/apps/discord-bridge/src/state.ts index 1f0014f..16873b3 100644 --- a/apps/discord-bridge/src/state.ts +++ b/apps/discord-bridge/src/state.ts @@ -113,6 +113,7 @@ function parseGateway(value: unknown): DiscordGatewayState | undefined { mainThreadId: optionalString(value.mainThreadId), statusMessageId: optionalString(value.statusMessageId), createdAt: optionalString(value.createdAt), + toolsVersion: optionalNumber(value.toolsVersion), delegations: Array.isArray(value.delegations) ? value.delegations.map(parseGatewayDelegation) : [], diff --git a/apps/discord-bridge/src/types.ts b/apps/discord-bridge/src/types.ts index ef3a604..d2044ca 100644 --- a/apps/discord-bridge/src/types.ts +++ b/apps/discord-bridge/src/types.ts @@ -149,6 +149,7 @@ export type DiscordGatewayState = { mainThreadId?: string; statusMessageId?: string; createdAt?: string; + toolsVersion?: number; delegations: DiscordGatewayDelegation[]; }; diff --git a/apps/discord-bridge/test/bridge.test.ts b/apps/discord-bridge/test/bridge.test.ts index 48f0131..b2094ed 100644 --- a/apps/discord-bridge/test/bridge.test.ts +++ b/apps/discord-bridge/test/bridge.test.ts @@ -72,6 +72,7 @@ describe("DiscordCodexBridge", () => { expect.objectContaining({ homeChannelId: "home-channel", mainThreadId: "codex-thread-1", + toolsVersion: 1, }), ); expect(bridge.stateForTest().sessions[0]).toEqual( @@ -293,6 +294,64 @@ describe("DiscordCodexBridge", () => { await bridge.stop(); }); + test("replaces stale persisted gateway sessions when no main thread is configured", async () => { + const client = new FakeCodexClient(); + const transport = new FakeDiscordTransport(); + const store = new MemoryStateStore({ + ...emptyState(), + gateway: { + homeChannelId: "home-channel", + mainThreadId: "old-codex-thread", + createdAt: "2026-05-13T00:00:00.000Z", + delegations: [], + }, + sessions: [ + { + discordThreadId: "home-channel", + parentChannelId: "home-channel", + codexThreadId: "old-codex-thread", + title: "Codex Gateway", + createdAt: "2026-05-13T00:00:00.000Z", + cwd: "/workspace", + mode: "gateway", + }, + ], + }); + const bridge = new DiscordCodexBridge({ + client, + transport, + store, + config: testConfig({ + gateway: { homeChannelId: "home-channel" }, + }), + }); + + await bridge.start(); + await waitFor(() => bridge.stateForTest().gateway?.mainThreadId === "codex-thread-1"); + + expect(client.resumeThreadCalls).toEqual([]); + expect(client.startThreadCalls).toHaveLength(1); + expect(client.startThreadCalls[0]?.dynamicTools).toEqual( + expect.arrayContaining([ + expect.objectContaining({ namespace: "codex_gateway" }), + ]), + ); + expect(bridge.stateForTest().sessions.filter((session) => + session.mode === "gateway" + )).toEqual([ + expect.objectContaining({ + codexThreadId: "codex-thread-1", + }), + ]); + expect(bridge.stateForTest().gateway).toEqual( + expect.objectContaining({ + mainThreadId: "codex-thread-1", + toolsVersion: 1, + }), + ); + await bridge.stop(); + }); + test("routes bot mentions in the home channel to the gateway instead of creating threads", async () => { const client = new FakeCodexClient(); const transport = new FakeDiscordTransport(); diff --git a/apps/discord-bridge/test/state.test.ts b/apps/discord-bridge/test/state.test.ts index 35671b2..69ce86a 100644 --- a/apps/discord-bridge/test/state.test.ts +++ b/apps/discord-bridge/test/state.test.ts @@ -19,6 +19,7 @@ describe("JsonFileStateStore", () => { mainThreadId: "codex-gateway-thread", statusMessageId: "message-gateway-status", createdAt: "2026-05-11T00:00:00.000Z", + toolsVersion: 1, delegations: [ { id: "delegation-1", @@ -86,6 +87,7 @@ describe("JsonFileStateStore", () => { mainThreadId: "codex-gateway-thread", statusMessageId: "message-gateway-status", createdAt: "2026-05-11T00:00:00.000Z", + toolsVersion: 1, delegations: [ { id: "delegation-1",