From d6c7d30bd45ad134b6eb00b97250536250cdbb1e Mon Sep 17 00:00:00 2001 From: Devvyn <22340871+opticon454@users.noreply.github.com> Date: Fri, 30 Oct 2026 16:42:33 +0800 Subject: [PATCH] fix(discord): tidy a thread made in the old channel, keep the two pending sets in step, settle the hint when the settings are off --- src/discord-bridge.ts | 11 +++++++++-- test/discord-bridge.test.ts | 20 +++++++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/discord-bridge.ts b/src/discord-bridge.ts index 2ec15282..29095f94 100644 --- a/src/discord-bridge.ts +++ b/src/discord-bridge.ts @@ -766,6 +766,7 @@ export class DiscordBridge { if (parent && parent !== this.threadsParent) { this.threads = {}; this.pending.clear(); + this.closedWhilePending.clear(); this.threadsParent = parent; await this.persistThreads(); } @@ -840,7 +841,10 @@ export class DiscordBridge { const t = await this.api.createThread(parent, threadNameFor(name, sessionId)); if (!t?.id) return this.fail(new BridgeProblem('Discord did not return a thread')); // A save moved the bridge to another channel meanwhile: this thread belongs to the old one. - if (parent !== this.cfg.parentChannelId) return this.fail(new BridgeProblem('The parent channel changed')); + if (parent !== this.cfg.parentChannelId) { + await this.closeThread(t.id); // made in the old channel: tidy it + return this.fail(new BridgeProblem('The parent channel changed')); + } if (this.closedWhilePending.has(sessionId)) { await this.closeThread(t.id); return this.fail(new BridgeProblem('The tab was closed')); @@ -950,10 +954,13 @@ export class DiscordBridge { */ async sessionIdle(sessionId: string, hintable: boolean): Promise { if (!this.fresh.has(sessionId)) return; + if (!hintable || !this.cfg.enabled || !this.cfg.startupHint || !this.cfg.typedHint) { + this.fresh.delete(sessionId); // no longer new: turning the hint on later must not reach this tab + return; + } // A dialog is up (codex's trust prompt reads as idle): Enter would pick its highlighted option, so wait for the next idle. if (this.deps.awaitingChoice?.(sessionId)) return; this.fresh.delete(sessionId); - if (!hintable || !this.cfg.enabled || !this.cfg.startupHint || !this.cfg.typedHint) return; const text = sanitiseReply(`${DISCORD_STARTUP_HINT} Reply with just: ok`); // fromUser false: this is Codeman talking, so it must not auto-name the tab or count as the person's input. if (text) await this.deps.sendInput(sessionId, text, false).catch(() => false); diff --git a/test/discord-bridge.test.ts b/test/discord-bridge.test.ts index be45ea27..8b24d71e 100644 --- a/test/discord-bridge.test.ts +++ b/test/discord-bridge.test.ts @@ -533,11 +533,16 @@ describe('DiscordBridge', () => { it('drops a thread whose creation finished after the parent channel changed', async () => { let release!: () => void; const gate = new Promise((r) => (release = r)); - const b = bridge({}, async (url) => { + const b = bridge({}, async (url, init) => { if (String(url).endsWith(`/channels/${PARENT}/threads`)) { await gate; return jsonRes({ id: THREAD }); } + posts.push({ + path: new URL(String(url)).pathname.replace('/api/v10', ''), + method: String(init.method), + body: init.body ? JSON.parse(String(init.body)) : undefined, + }); return jsonRes({ id: 'msg' }); }); const first = b.notify(MSG); @@ -546,6 +551,8 @@ describe('DiscordBridge', () => { await b.reload(); release(); expect(await first).toMatchObject({ ok: false }); + // The thread made in the old channel is tidied, not left open there. + expect(posts).toContainEqual({ path: `/channels/${THREAD}`, method: 'PATCH', body: { archived: true } }); expect(await readThreadMap(dir)).toEqual({}); b.stop(); }); @@ -943,6 +950,17 @@ describe('DiscordBridge files and startup hint', () => { b.stop(); }); + it('does not hint a tab that was already past its first idle while the hint was off', async () => { + const off = await make({ typedHint: false }); + await off.sessionCreated('s8', 'w8', t - 1000); + await off.sessionIdle('s8', true); // first idle with the hint off: no longer new + await writeDiscordConfig(dir, { ...CFG, typedHint: true }); + await off.reload(); + await off.sessionIdle('s8', true); + expect(sent).toHaveLength(0); + off.stop(); + }); + it('says the agent has exited and types nothing when the tab reports it', async () => { const b = await make({}, undefined, undefined, () => true); await b.onGatewayMessage(reply({ content: 'hello', attachments: [] }));