{"record":{"id":"8ec6367ab2d70481","repo":"moeru-ai/airi","slug":"the-extension-management-window-is-no-longer-available","errorCode":null,"errorMessage":"The Extension management window is no longer available.","messagePattern":"The Extension management window is no longer available\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/stage-tamagotchi/src/main/services/airi/plugins/index.ts","lineNumber":128,"sourceCode":"  const invokePluginProtocolListProviders = defineInvoke(context, pluginProtocolListProviders)\n  // A plan remains owned by the renderer that displayed its review. Reopening\n  // Settings creates a new renderer, which must start a new import review.\n  const directoryImportPlanOwners = new Map<string, number>()\n  const trackedDirectoryImportOwnerIds = new Set<number>()\n\n  defineInvokeHandler(context, electronPluginList, async () => {\n    return await hostService.list()\n  })\n\n  defineInvokeHandler(context, electronPluginPrepareDirectoryImport, async (_, invokeOptions) => {\n    const event = requireExtensionManagementEvent(invokeOptions, options.getExtensionManagementWebContentsId)\n    const dialogOptions: OpenDialogOptions = {\n      properties: ['openDirectory'],\n      securityScopedBookmarks: true,\n    }\n    const ownerWindow = BrowserWindow.fromWebContents(event.sender)\n    if (!ownerWindow) {\n      throw new Error('The Extension management window is no longer available.')\n    }\n    trackDirectoryImportOwner(\n      hostService,\n      directoryImportPlanOwners,\n      trackedDirectoryImportOwnerIds,\n      ownerWindow,\n      event.sender.id,\n    )\n    const selection = await dialog.showOpenDialog(ownerWindow, dialogOptions)\n    const sourcePath = selection.filePaths[0]\n    if (selection.canceled || !sourcePath) {\n      return { status: 'cancelled' as const }\n    }\n\n    const plan = await hostService.prepareDirectoryImport(sourcePath, selection.bookmarks?.[0])\n    if (\n      ownerWindow.isDestroyed()\n      || options.getExtensionManagementWebContentsId?.() !== event.sender.id","sourceCodeStart":110,"sourceCodeEnd":146,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/apps/stage-tamagotchi/src/main/services/airi/plugins/index.ts#L110-L146","documentation":"Thrown during the directory-import flow when BrowserWindow.fromWebContents(event.sender) returns null, meaning the window that owned the sending webContents no longer exists. The flow needs the owner window for security-scoped bookmark tracking and ownership bookkeeping, so it aborts instead of continuing with a detached sender.","triggerScenarios":"The user closes the Extension management window between starting the directory-picker IPC call and the point where the main process resolves the owning BrowserWindow, or the sender is a frameless/webview context with no associated BrowserWindow.","commonSituations":"User closes the settings/extension window while a native folder dialog operation is in flight; window destroyed mid-request during app shutdown; sender webview detached from its host window.","solutions":["Keep the Extension management window open until the folder-import request completes.","Guard the renderer side: disable the import action on window close and check window liveness before invoking.","Handle the rejection in the renderer gracefully (it cannot recover once the window is gone)."],"exampleFix":"// before\nconst plan = await window.ipc.invoke('extension:select-folder') // invoked just before window.close()\nwindow.close()\n\n// after\nconst plan = await window.ipc.invoke('extension:select-folder')\nif (plan) window.close()","handlingStrategy":"try-catch","validationCode":"if (ownerWindow?.isDestroyed()) throw new Error('window gone; abort import before invoking')","typeGuard":"function hasAliveWindow(win: Electron.BrowserWindow | null): win is Electron.BrowserWindow {\n  return win !== null && !win.isDestroyed()\n}","tryCatchPattern":"try {\n  const plan = await window.ipc.invoke('extension:select-folder')\n} catch (err) {\n  if (String((err as Error).message).includes('no longer available')) {\n    // window closed mid-request; surface a neutral message to the user\n  }\n}","preventionTips":["Prevent window close while a folder-picker request is in flight.","Check win.isDestroyed() before dependent operations.","Avoid invoking import handlers from webviews or detached frames.","Handle in-flight promises on window 'close' events."],"tags":["electron","ipc","lifecycle","window"],"backgroundTag":"invalid-state-transition","analyzedSha":"438a067dde47aa0bdb46c2323d1fe293dc805218","analyzedAt":"2026-09-17T01:14:42.644Z","contentChangedAt":"2026-09-17T01:14:42.644Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}