From 65ef6e8830320c025dfc29b8fb788e179bff72d7 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 21 Aug 2026 12:58:04 +0900 Subject: [PATCH] fix(dashboard): bind editMessage to channel to stop repost loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The retry refactor captured `const editMessage = channel.editMessage` and called it detached, losing `this`. Every status edit then threw "this.client is undefined", so each cycle failed all retries and reposted a fresh (notifying) status message — ~50 reposts in 30 minutes. Bind the method to the channel so the edit runs in place. Adds a regression test showing a detached method fails while a bound one succeeds. Co-Authored-By: Claude Opus 4.7 --- src/unified-dashboard.test.ts | 35 +++++++++++++++++++++++++++++++++++ src/unified-dashboard.ts | 5 ++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/unified-dashboard.test.ts b/src/unified-dashboard.test.ts index e1a5358..69d349f 100644 --- a/src/unified-dashboard.test.ts +++ b/src/unified-dashboard.test.ts @@ -312,6 +312,41 @@ describe('editStatusMessageWithRetry', () => { expect(ok).toBe(false); expect(slept).toBe(0); }); + + it('works with a channel method that relies on `this` when bound (regression)', async () => { + class FakeChannel { + client = { ok: true }; + edits = 0; + async editMessage(): Promise { + // Mirrors the real editMessage: throws if `this` is lost. + if (!this.client) throw new Error('this.client is undefined'); + this.edits++; + } + } + const channel = new FakeChannel(); + + // A detached method reference loses `this` → every attempt fails. + const detached = channel.editMessage; + const detachedOk = await editStatusMessageWithRetry({ + editOnce: () => detached(), + maxRetries: 0, + retryDelayMs: 0, + sleep: async () => {}, + }); + expect(detachedOk).toBe(false); + expect(channel.edits).toBe(0); + + // Binding to the channel (the fix) preserves `this` and succeeds. + const bound = channel.editMessage.bind(channel); + const boundOk = await editStatusMessageWithRetry({ + editOnce: () => bound(), + maxRetries: 0, + retryDelayMs: 0, + sleep: async () => {}, + }); + expect(boundOk).toBe(true); + expect(channel.edits).toBe(1); + }); }); describe('createCoalescingTrigger', () => { diff --git a/src/unified-dashboard.ts b/src/unified-dashboard.ts index df28687..ff92248 100644 --- a/src/unified-dashboard.ts +++ b/src/unified-dashboard.ts @@ -916,7 +916,10 @@ export async function startUnifiedDashboard( // immediately spawn a fresh status message. Retry the edit a couple of // times with a delay first; only give up (and repost) if all fail. const editId = statusMessageId; - const editMessage = channel.editMessage; + // Bind to the channel: a detached method reference loses `this` and the + // edit throws ("this.client is undefined"), which would make every + // update fail and repost a fresh (notifying) message. + const editMessage = channel.editMessage.bind(channel); // First render after start: 0 retries → repost immediately if the edit // fails. Steady state: retry twice at 15s before reposting. const maxRetries = firstStatusRender ? 0 : STATUS_EDIT_MAX_RETRIES;