{"record":{"id":"6ba72a957ed931aa","repo":"koala73/worldmonitor","slug":"search-manager-destroyed","errorCode":null,"errorMessage":"Search manager destroyed","messagePattern":"Search manager destroyed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/app/webmcp-search-controller.ts","lineNumber":126,"sourceCode":"\n  /** Cancel renderer-readiness and selection phases of the current result open. */\n  public cancelPendingOpen(): void {\n    this.openOperationEpoch += 1;\n    const active = this.activeOpenOperation;\n    this.activeOpenOperation = null;\n    active?.controller.abort();\n    this.bindings.cancelPendingSelection();\n  }\n\n  public async search(\n    query: string,\n    scope: DashboardSearchScope,\n    limit: number,\n    signal?: AbortSignal,\n  ): Promise<DashboardSearchResponse> {\n    await raceWebMcpAbort(this.bindings.waitForIndexReady(), signal);\n    throwIfWebMcpAborted(signal);\n    if (this.bindings.isDestroyed()) throw new Error('Search manager destroyed');\n    this.bindings.refreshIndex();\n    const modal = this.bindings.getModal();\n    if (!modal) throw new Error('Search index is not initialised');\n\n    let searchResult = modal.search(query, scope as SearchScope);\n    if (\n      searchResult.flightCallsign\n      && searchResult.orderedMatches.length === 0\n      && this.bindings.hasPremiumAccess()\n    ) {\n      try {\n        await raceWebMcpAbort(\n          this.bindings.fetchLiveFlight(searchResult.flightCallsign, signal),\n          signal,\n        );\n        throwIfWebMcpAborted(signal);\n        if (this.bindings.isDestroyed()) throw new Error('Search manager destroyed');\n        this.bindings.refreshIndex();","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/koala73/worldmonitor/blob/9361220cc013571781071f0206e4d80fd14b2f7f/src/app/webmcp-search-controller.ts#L108-L144","documentation":"Thrown by WebMcpSearchController.search() when, after awaiting bindings.waitForIndexReady(), bindings.isDestroyed() is true. destroy() already ran on the controller (unsubscribers cleared, result cache emptied), so an in-flight or subsequent search refuses to proceed: its index, modal, and subscriptions may be gone.","triggerScenarios":"search() is invoked and awaits index readiness; during that await the app (and thus the controller) is destroyed — teardown of the WebMCP search feature racing the first (or any) search call. A search issued after destroy() hits the same check immediately.","commonSituations":"Tab unload or HMR teardown during an agent's search_dashboard call; tests that destroy the controller while a search promise is pending; sequential tool calls where the second arrives after cleanup began.","solutions":["Catch this error and treat it as terminal for the controller — do not retry search on a destroyed instance; re-create the controller with the app","Add an AbortSignal to search() and destroy() so teardown cancels in-flight searches explicitly instead of relying on the post-await check","Queue or reject new searches at the entry point when bindings.isDestroyed() is already true, before awaiting index readiness"],"exampleFix":"// before\nconst response = await controller.search(query, scope, limit); // unhandled throw after destroy\n\n// after\nif (controller.isDestroyed()) return { ok: false, status: 'denied', reason: 'search_manager_destroyed' };\ntry {\n  const response = await controller.search(query, scope, limit);\n} catch (e) {\n  if (e instanceof Error && e.message === 'Search manager destroyed') return deniedResult();\n  throw e;\n}","handlingStrategy":"type-guard","validationCode":"if (controller.isDestroyed()) { // check before entry AND after every await\n  return { ok: false, status: 'denied', reason: 'search_manager_destroyed' };\n}","typeGuard":"function isSearchManagerUsable(controller: { isDestroyed(): boolean }): boolean {\n  return !controller.isDestroyed();\n}","tryCatchPattern":"try { return await controller.search(q, scope, limit); } catch (e) { if (e instanceof Error && e.message === 'Search manager destroyed') return DESTROYED_RESULT; throw e; }","preventionTips":["Add an AbortSignal to search()/destroy() so teardown cancels in-flight work instead of surfacing as this error","Reject new searches at the entry point once destroyed","Re-create the controller together with the app, never independently"],"tags":["webmcp","agent-tools","search","lifecycle","race-condition"],"backgroundTag":"stale-app-instance","analyzedSha":"9361220cc013571781071f0206e4d80fd14b2f7f","analyzedAt":"2026-08-27T19:53:08.521Z","contentChangedAt":"2026-08-27T19:53:08.521Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}