{"record":{"id":"600fddbce6658674","repo":"koala73/worldmonitor","slug":"app-destroyed","errorCode":"app_destroyed","errorMessage":"Dashboard is no longer available.","messagePattern":"Dashboard is no longer available\\.","errorType":"exception","errorClass":"DashboardBindingError","httpStatus":null,"severity":"error","filePath":"src/App.ts","lineNumber":1832,"sourceCode":"            getPanelConfig: (panelId) => getEffectivePanelConfig(panelId, SITE_VARIANT),\n            isPanelAllowed: (panelId, config) => (\n              isPanelEntitled(panelId, config, hasPremiumAccess(getAuthState()))\n            ),\n            hasPremiumAccess: () => hasPremiumAccess(getAuthState()),\n            applyViewChange: (viewAction) => {\n              if (viewAction.view) trackMapViewChange(viewAction.view);\n            },\n            applyLayerChange: (layer, enabled, source) => (\n              this.eventHandlers.applyMapLayerChange(layer, enabled, source)\n            ),\n          },\n          syncUrlStateNow: () => this.eventHandlers.syncUrlStateNow(),\n        });\n      },\n      searchDashboard: async (query, scope, limit) => {\n        await this.waitForDashboardReady(false);\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 this.ensureSearchManager();\n        } catch (error) {\n          if (this.state.isDestroyed) {\n            throw new DashboardBindingError('app_destroyed', 'Dashboard is no longer available.');\n          }\n          throw error;\n        }\n        if (this.state.isDestroyed) {\n          throw new DashboardBindingError('app_destroyed', 'Dashboard is no longer available.');\n        }\n        return manager.searchDashboard(query, scope, limit);\n      },\n      openSearchResult: async (resultKey) => {\n        // A capability can only exist after search_dashboard initialized the\n        // manager. Deny fabricated first-use keys without loading the lazy","sourceCodeStart":1814,"sourceCodeEnd":1850,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/src/App.ts#L1814-L1850","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":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-23T16:17:53.355Z"}