koala73/worldmonitor · error · DashboardBindingError
app_destroyed
app_destroyed
Error message
Dashboard is no longer available.
What it means
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.
Source
Thrown at src/App.ts:1832
getPanelConfig: (panelId) => getEffectivePanelConfig(panelId, SITE_VARIANT),
isPanelAllowed: (panelId, config) => (
isPanelEntitled(panelId, config, hasPremiumAccess(getAuthState()))
),
hasPremiumAccess: () => hasPremiumAccess(getAuthState()),
applyViewChange: (viewAction) => {
if (viewAction.view) trackMapViewChange(viewAction.view);
},
applyLayerChange: (layer, enabled, source) => (
this.eventHandlers.applyMapLayerChange(layer, enabled, source)
),
},
syncUrlStateNow: () => this.eventHandlers.syncUrlStateNow(),
});
},
searchDashboard: async (query, scope, limit) => {
await this.waitForDashboardReady(false);
if (this.state.isDestroyed) {
throw new DashboardBindingError('app_destroyed', 'Dashboard is no longer available.');
}
let manager: SearchManager;
try {
manager = await this.ensureSearchManager();
} catch (error) {
if (this.state.isDestroyed) {
throw new DashboardBindingError('app_destroyed', 'Dashboard is no longer available.');
}
throw error;
}
if (this.state.isDestroyed) {
throw new DashboardBindingError('app_destroyed', 'Dashboard is no longer available.');
}
return manager.searchDashboard(query, scope, limit);
},
openSearchResult: async (resultKey) => {
// A capability can only exist after search_dashboard initialized the
// manager. Deny fabricated first-use keys without loading the lazyView on GitHub (pinned to eeab0a219f)
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
Example fix
// before
try { await app.searchDashboard(q, scope, limit); } catch (e) { retry(3); } // pointless: instance is dead
// after
try {
await app.searchDashboard(q, scope, limit);
} catch (e) {
if (e instanceof DashboardBindingError && e.code === 'app_destroyed') {
await reconnectToFreshApp(); // terminal for this instance
} else throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
if (app.state.isDestroyed) return { ok: false, status: 'denied', reason: 'app_destroyed' }; Type guard
function isAppAlive(app: { state: { isDestroyed: boolean } }): boolean {
return !app.state.isDestroyed;
} Try / catch
try { await app.searchDashboard(q, scope, limit); } catch (e) { if (e instanceof DashboardBindingError && e.code === 'app_destroyed') { session.end(); } else throw e; } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Dashboard is no longer available.
- Search manager destroyed
- app_destroyed
- Country brief panel is not initialised
- map_unavailable
AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21).
Data as JSON: /api/errors/600fddbce6658674.
Report an issue: GitHub.