{"record":{"id":"600fddbce6658674","repo":"koala73/worldmonitor","slug":"app-destroyed","errorCode":"app_destroyed","errorMessage":"app_destroyed","messagePattern":"app_destroyed","errorType":"error_code","errorClass":"DashboardBindingError","httpStatus":null,"severity":"error","filePath":"src/App.ts","lineNumber":2108,"sourceCode":"              if (viewAction.view) trackMapViewChange(viewAction.view);\n            },\n            getMapLayerRuntimeAvailability: this.getMapLayerRuntimeAvailability,\n            applyLayerChange: (layer, enabled, source) => (\n              this.eventHandlers.applyMapLayerChange(layer, enabled, source)\n            ),\n            requireMapModePersistence: true,\n          },\n          syncUrlStateNow: () => this.eventHandlers.syncUrlStateNow(),\n        });\n      },\n      selectPanelTab: async (panelId, tab, execution) => {\n        return selectWebMcpPanelTab(this.state.panels, panelId, tab, {\n          waitForUiReady: () => this.waitForDashboardReady(false, execution?.signal),\n          prepareTab: (_selectedPanelId, selectedTab, signal) => (\n            selectedTab === 'physical'\n              ? this.dataLoader.loadPhysicalPremiumComparison(signal)\n              : undefined\n          ),\n          signal: execution?.signal,\n        });\n      },\n      searchDashboard: async (query, scope, limit, execution) => {\n        await this.waitForDashboardReady(false, execution?.signal);\n        throwIfWebMcpAborted(execution?.signal);\n        if (this.state.isDestroyed) {\n          throw new DashboardBindingError('app_destroyed', 'Dashboard is no longer available.');\n        }\n        let manager: SearchManager;\n        try {\n          manager = await raceWebMcpAbort(\n            this.ensureSearchManager(),\n            execution?.signal,\n          );\n          throwIfWebMcpAborted(execution?.signal);\n        } catch (error) {\n          if (this.state.isDestroyed) {","sourceCodeStart":2090,"sourceCodeEnd":2126,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/src/App.ts#L2090-L2126","documentation":"DashboardBindingError with code 'app_destroyed', thrown by the search_dashboard WebMCP binding right after waitForDashboardReady(false) resolves: state.isDestroyed is true, so the app instance was torn down between the readiness await and this check. It marks the binding as terminally unavailable — retrying against the same App instance cannot succeed.","triggerScenarios":"An agent calls search_dashboard concurrently with app teardown: SPA route teardown, page unload, or Vite HMR replacing the app while the dashboard-ready promise was pending. The readiness await resolved only after destroy() had already flipped isDestroyed.","commonSituations":"Dev-mode hot module reload while an agent session is connected; end-to-end tests that destroy() the App between tool invocations; the user closing the tab or navigating away mid tool-call.","solutions":["Catch DashboardBindingError and branch on its code: for 'app_destroyed', stop retrying and re-establish the session against a fresh App instance","In dev workflows, reconnect the MCP/agent client after HMR instead of reusing the old tool bindings","Ensure destroy() runs only after in-flight tool calls drain, if teardown ordering is under your control"],"exampleFix":"// before\ntry { await app.searchDashboard(q, scope, limit); } catch (e) { retry(3); } // pointless: instance is dead\n\n// after\ntry {\n  await app.searchDashboard(q, scope, limit);\n} catch (e) {\n  if (e instanceof DashboardBindingError && e.code === 'app_destroyed') {\n    await reconnectToFreshApp(); // terminal for this instance\n  } else throw e;\n}","handlingStrategy":"try-catch","validationCode":"if (app.state.isDestroyed) return { ok: false, status: 'denied', reason: 'app_destroyed' };","typeGuard":"function isAppAlive(app: { state: { isDestroyed: boolean } }): boolean {\n  return !app.state.isDestroyed;\n}","tryCatchPattern":"try { await app.searchDashboard(q, scope, limit); } catch (e) { if (e instanceof DashboardBindingError && e.code === 'app_destroyed') { session.end(); } else throw e; }","preventionTips":["Treat 'app_destroyed' as terminal — never retry against the same instance","In tests, await tool promises before destroy()","Re-derive agent tool bindings after HMR instead of reusing them"],"tags":["webmcp","agent-tools","lifecycle","race-condition"],"backgroundTag":"stale-app-instance","analyzedSha":"e586b8b4b80f595aa7ece295eec10d76f2921240","analyzedAt":"2026-08-27T19:53:08.521Z","contentChangedAt":"2026-08-27T19:53:08.521Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}