Refresh stale Discord gateway sessions

This commit is contained in:
matamune 2026-05-14 14:48:32 +00:00
parent bec3641b1c
commit 9d64886259
Signed by: matamune
GPG key ID: 3BB8E7D3B968A324
5 changed files with 82 additions and 4 deletions

View file

@ -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);

View file

@ -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)
: [],

View file

@ -149,6 +149,7 @@ export type DiscordGatewayState = {
mainThreadId?: string;
statusMessageId?: string;
createdAt?: string;
toolsVersion?: number;
delegations: DiscordGatewayDelegation[];
};

View file

@ -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();

View file

@ -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",