From f34e70623f90943f26caf44ef96ed49ea6f3a81e Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 2 Dec 2025 18:27:54 -0500 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=A4=96=20fix:=20prevent=20IPC=20send?= =?UTF-8?q?=20to=20destroyed=20window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add isDestroyed() checks before all mainWindow.webContents.send() calls to prevent 'Object has been destroyed' errors when the window is closed while background processes (like init stderr streams) are still emitting events. Fixes crash when closing window during workspace initialization. --- src/node/services/ipcMain.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/node/services/ipcMain.ts b/src/node/services/ipcMain.ts index 6129544767..4fd3c720fe 100644 --- a/src/node/services/ipcMain.ts +++ b/src/node/services/ipcMain.ts @@ -228,7 +228,7 @@ export class IpcMain { workspaceId: string, snapshot: WorkspaceActivitySnapshot | null ): void { - if (!this.mainWindow) { + if (!this.mainWindow || this.mainWindow.isDestroyed()) { return; } this.mainWindow.webContents.send(IPC_CHANNELS.WORKSPACE_ACTIVITY, { @@ -508,7 +508,7 @@ export class IpcMain { }); const chatUnsubscribe = session.onChatEvent((event) => { - if (!this.mainWindow) { + if (!this.mainWindow || this.mainWindow.isDestroyed()) { return; } const channel = getChatChannel(event.workspaceId); @@ -516,7 +516,7 @@ export class IpcMain { }); const metadataUnsubscribe = session.onMetadataEvent((event) => { - if (!this.mainWindow) { + if (!this.mainWindow || this.mainWindow.isDestroyed()) { return; } this.mainWindow.webContents.send(IPC_CHANNELS.WORKSPACE_METADATA, { @@ -969,7 +969,7 @@ export class IpcMain { const session = this.sessions.get(workspaceId); if (session) { session.emitMetadata(updatedMetadata); - } else if (this.mainWindow) { + } else if (this.mainWindow && !this.mainWindow.isDestroyed()) { this.mainWindow.webContents.send(IPC_CHANNELS.WORKSPACE_METADATA, { workspaceId, metadata: updatedMetadata, @@ -1379,7 +1379,9 @@ export class IpcMain { type: "delete", historySequences: deletedSequences, }; - this.mainWindow.webContents.send(getChatChannel(workspaceId), deleteMessage); + if (this.mainWindow && !this.mainWindow.isDestroyed()) { + this.mainWindow.webContents.send(getChatChannel(workspaceId), deleteMessage); + } } return { success: true, data: undefined }; @@ -1417,7 +1419,7 @@ export class IpcMain { } // Send delete event to frontend for all old messages - if (deletedSequences.length > 0 && this.mainWindow) { + if (deletedSequences.length > 0 && this.mainWindow && !this.mainWindow.isDestroyed()) { const deleteMessage: DeleteMessage = { type: "delete", historySequences: deletedSequences, @@ -1426,7 +1428,7 @@ export class IpcMain { } // Send the new summary message to frontend - if (this.mainWindow) { + if (this.mainWindow && !this.mainWindow.isDestroyed()) { this.mainWindow.webContents.send(getChatChannel(workspaceId), summaryMessage); } @@ -1632,7 +1634,7 @@ export class IpcMain { const existingSession = this.sessions.get(workspaceId); if (existingSession) { existingSession.emitMetadata(null); - } else if (this.mainWindow) { + } else if (this.mainWindow && !this.mainWindow.isDestroyed()) { this.mainWindow.webContents.send(IPC_CHANNELS.WORKSPACE_METADATA, { workspaceId, metadata: null, @@ -2091,7 +2093,7 @@ export class IpcMain { const chatChannel = getChatChannel(workspaceId); await session.replayHistory((event) => { - if (!this.mainWindow) { + if (!this.mainWindow || this.mainWindow.isDestroyed()) { return; } this.mainWindow.webContents.send(chatChannel, event.message); From b58442f39e02868f1a8be517eeca4cea045464b0 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 2 Dec 2025 18:35:37 -0500 Subject: [PATCH 2/5] trigger CI From ce149eb4741af230605aec717f0eed66e8003c33 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 2 Dec 2025 18:41:21 -0500 Subject: [PATCH 3/5] fix: use optional chaining for isDestroyed() checks --- src/node/services/ipcMain.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/node/services/ipcMain.ts b/src/node/services/ipcMain.ts index 4fd3c720fe..2434a1600c 100644 --- a/src/node/services/ipcMain.ts +++ b/src/node/services/ipcMain.ts @@ -228,7 +228,7 @@ export class IpcMain { workspaceId: string, snapshot: WorkspaceActivitySnapshot | null ): void { - if (!this.mainWindow || this.mainWindow.isDestroyed()) { + if (!this.mainWindow || this.mainWindow?.isDestroyed()) { return; } this.mainWindow.webContents.send(IPC_CHANNELS.WORKSPACE_ACTIVITY, { @@ -508,7 +508,7 @@ export class IpcMain { }); const chatUnsubscribe = session.onChatEvent((event) => { - if (!this.mainWindow || this.mainWindow.isDestroyed()) { + if (!this.mainWindow || this.mainWindow?.isDestroyed()) { return; } const channel = getChatChannel(event.workspaceId); @@ -516,7 +516,7 @@ export class IpcMain { }); const metadataUnsubscribe = session.onMetadataEvent((event) => { - if (!this.mainWindow || this.mainWindow.isDestroyed()) { + if (!this.mainWindow || this.mainWindow?.isDestroyed()) { return; } this.mainWindow.webContents.send(IPC_CHANNELS.WORKSPACE_METADATA, { @@ -969,7 +969,7 @@ export class IpcMain { const session = this.sessions.get(workspaceId); if (session) { session.emitMetadata(updatedMetadata); - } else if (this.mainWindow && !this.mainWindow.isDestroyed()) { + } else if (this.mainWindow && !this.mainWindow?.isDestroyed()) { this.mainWindow.webContents.send(IPC_CHANNELS.WORKSPACE_METADATA, { workspaceId, metadata: updatedMetadata, @@ -1379,7 +1379,7 @@ export class IpcMain { type: "delete", historySequences: deletedSequences, }; - if (this.mainWindow && !this.mainWindow.isDestroyed()) { + if (this.mainWindow && !this.mainWindow?.isDestroyed()) { this.mainWindow.webContents.send(getChatChannel(workspaceId), deleteMessage); } } @@ -1419,7 +1419,7 @@ export class IpcMain { } // Send delete event to frontend for all old messages - if (deletedSequences.length > 0 && this.mainWindow && !this.mainWindow.isDestroyed()) { + if (deletedSequences.length > 0 && this.mainWindow && !this.mainWindow?.isDestroyed()) { const deleteMessage: DeleteMessage = { type: "delete", historySequences: deletedSequences, @@ -1428,7 +1428,7 @@ export class IpcMain { } // Send the new summary message to frontend - if (this.mainWindow && !this.mainWindow.isDestroyed()) { + if (this.mainWindow && !this.mainWindow?.isDestroyed()) { this.mainWindow.webContents.send(getChatChannel(workspaceId), summaryMessage); } @@ -1634,7 +1634,7 @@ export class IpcMain { const existingSession = this.sessions.get(workspaceId); if (existingSession) { existingSession.emitMetadata(null); - } else if (this.mainWindow && !this.mainWindow.isDestroyed()) { + } else if (this.mainWindow && !this.mainWindow?.isDestroyed()) { this.mainWindow.webContents.send(IPC_CHANNELS.WORKSPACE_METADATA, { workspaceId, metadata: null, @@ -2093,7 +2093,7 @@ export class IpcMain { const chatChannel = getChatChannel(workspaceId); await session.replayHistory((event) => { - if (!this.mainWindow || this.mainWindow.isDestroyed()) { + if (!this.mainWindow || this.mainWindow?.isDestroyed()) { return; } this.mainWindow.webContents.send(chatChannel, event.message); From 2315bc182782730b47aab7a9d7a5c6c308cd2b2a Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 2 Dec 2025 18:48:47 -0500 Subject: [PATCH 4/5] fix: add isDestroyed stub to mock BrowserWindow in tests --- tests/ipcMain/setup.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/ipcMain/setup.ts b/tests/ipcMain/setup.ts index 77e4cc1ca7..434feabd33 100644 --- a/tests/ipcMain/setup.ts +++ b/tests/ipcMain/setup.ts @@ -37,6 +37,7 @@ function createMockBrowserWindow(): { openDevTools: jest.fn(), } as unknown as WebContents, isMinimized: jest.fn(() => false), + isDestroyed: jest.fn(() => false), restore: jest.fn(), focus: jest.fn(), loadURL: jest.fn(), From f53d4ad29e1bcdcfee7188356fade49dadc8d14d Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 2 Dec 2025 18:54:44 -0500 Subject: [PATCH 5/5] retrigger CI