From 4616e50d4bd53db60fcae81a2bfaca546b83e5ca Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Sun, 19 Jul 2026 13:19:09 +0300 Subject: [PATCH] chat event bus: tolerate mid-flight session bundle swaps Co-Authored-By: Claude Fable 5 --- oxlint-suppressions.json | 3 --- src/state/messages/events/agent.ts | 39 ++++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 38c3b8accb..88a55fdd8d 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -1242,9 +1242,6 @@ "typescript/no-explicit-any": { "count": 2 }, - "typescript/no-floating-promises": { - "count": 4 - }, "typescript/no-unsafe-member-access": { "count": 2 } diff --git a/src/state/messages/events/agent.ts b/src/state/messages/events/agent.ts index b4a4a1269e..f2f976e3ac 100644 --- a/src/state/messages/events/agent.ts +++ b/src/state/messages/events/agent.ts @@ -39,7 +39,7 @@ export class MessagesEventBus { this.id = nanoid(3) this.chatClient = params.chatClient - this.init() + void this.init() } /** @@ -47,7 +47,8 @@ export class MessagesEventBus { * session bundle (new client identities, same DID) and disposes the old one, * whose fetch then throws. Every request reads `this.chatClient` per call, so * reassigning the field is enough to keep polling alive without tearing down - * the bus and its in-memory poll cursor. + * the bus and its in-memory poll cursor. init()/poll() also tolerate the + * disposed client's mid-flight rejection (see their catch blocks). */ updateClient(client: Client) { this.chatClient = client @@ -251,7 +252,7 @@ export class MessagesEventBus { * Initializing transitions us to Ready + resetPoll + emit connect. */ this.status = MessagesEventBusStatus.Initializing - this.init() + void this.init() } else { /* * A poll failed mid-session but we still have a valid cursor. Resume @@ -269,9 +270,11 @@ export class MessagesEventBus { private async init() { logger.debug(`init`, {}) + const requestClient = this.chatClient + try { const response = await networkRetry(2, () => { - return this.chatClient.call(chat.bsky.convo.getLog, {}) + return requestClient.call(chat.bsky.convo.getLog, {}) }) // throw new Error('UNCOMMENT TO TEST INIT FAILURE') @@ -288,6 +291,18 @@ export class MessagesEventBus { this.dispatch({event: MessagesEventBusDispatchEvent.Ready}) } catch (e: any) { + /* + * A bundle swap disposed the client mid-flight, so this rejection is + * expected. Re-run init() against the replacement client instead of + * erroring out. We're still in Initializing here (no dispatch happened on + * this path), so the retry seeds latestRev and dispatches Ready exactly + * once. + */ + if (requestClient !== this.chatClient) { + void this.init() + return + } + if (!isNetworkError(e) && !isErrorMaybeAppPasswordPermissions(e)) { logger.error(`init failed`, { safeMessage: e.message, @@ -336,11 +351,11 @@ export class MessagesEventBus { } private startPoll() { - if (!this.isPolling) this.poll() + if (!this.isPolling) void this.poll() this.pollIntervalRef = setInterval(() => { if (this.isPolling) return - this.poll() + void this.poll() }, this.pollInterval) } @@ -365,9 +380,11 @@ export class MessagesEventBus { let needsEmit = false let batch: chat.bsky.convo.getLog.$OutputBody['logs'] = [] + const requestClient = this.chatClient + try { const response = await networkRetry(2, () => { - return this.chatClient.call(chat.bsky.convo.getLog, { + return requestClient.call(chat.bsky.convo.getLog, { cursor: this.latestRev, }) }) @@ -396,6 +413,14 @@ export class MessagesEventBus { } } } catch (e: any) { + /* + * A bundle swap disposed the client mid-flight, so this rejection is + * expected. Skip the error dispatch (which would stop polling) and let the + * next interval poll with the replacement client. The finally block still + * clears isPolling, and the empty batch means nothing is emitted below. + */ + if (requestClient !== this.chatClient) return + if (!isNetworkError(e) && !isErrorMaybeAppPasswordPermissions(e)) { logger.warn(`poll events failed`, { safeMessage: e.message,