koala73/worldmonitor · error · Error
Dashboard is no longer available.
Error message
Dashboard is no longer available.
What it means
Plain Error (not DashboardBindingError) thrown by waitForWebMcpUiReady() when the appDestroyed promise wins the three-way race against uiReady and the timeout timer. The caller awaited UI initialization, but the app was destroyed during that wait — e.g. startup was torn down before Phase-4 UI init completed. Note the sibling outcomes: the timeout arm throws a different message ('... did not initialise within ...ms'), and applyWebMcpDashboardAction does not throw at all for a destroyed app — it returns APP_DESTROYED_RESULT.
Solutions
- Catch this error in the tool binding and return a terminal 'app destroyed' result to the agent instead of letting it escape as a 500-style tool error
- Delay tool registration or queue early tool calls until uiReady settles, so destroy-during-startup is handled by one place
- In tests, await the init promise before destroy() to eliminate the race
Example fix
// before
await waitForWebMcpUiReady(this.uiReady, this.appDestroyed, 10_000); // throws raw Error
// after
try {
await waitForWebMcpUiReady(this.uiReady, this.appDestroyed, 10_000);
} catch (error) {
return { ok: false, status: 'denied', reason: 'app_destroyed' } as const;
} Defensive patterns
Strategy: try-catch
Validate before calling
if (appDestroyed.settled) return DESTROYED_RESULT; // if you track the promise, check before awaiting await Promise.race([uiReady, appDestroyed, timeout]);
Try / catch
try { await waitForWebMcpUiReady(uiReady, appDestroyed, 10_000); } catch (e) { if (e instanceof Error && e.message === 'Dashboard is no longer available.') return DESTROYED_RESULT; if (e instanceof Error && e.message.includes('did not initialise within')) return TIMEOUT_RESULT; throw e; } Prevention
- Distinguish this destroy-during-startup error from the sibling timeout message before choosing a fix
- Queue early tool calls until init settles
- In e2e tests, await init before destroy
When it happens
Trigger: A WebMCP tool invoked during the startup window while the app is destroyed before uiReady resolves: user closes the tab during boot, HMR replaces the app mid-init, or tests destroy() the app while first paint is still pending. The awaited Promise.race resolves 'destroyed', which converts to this throw.
Common situations: Agent clients probing tools immediately on page load while the user navigates away; flaky e2e teardown that destroys the app before init finishes; double-mount/unmount cycles in dev frameworks.
Related errors
- app_destroyed
- Search manager destroyed
- app_destroyed
- Country brief panel is not initialised
- Dashboard is no longer available.
AI-assisted analysis of koala73/worldmonitor@9361220cc0 (2026-08-27).
Data as JSON: /api/errors/247b9eb8d32ecc57.
Report an issue: GitHub.
Appendix: source
Thrown at src/app/webmcp-dashboard.ts:197
rejectAbort = reject;
});
const handleAbort = (): void => {
try {
throwIfWebMcpAborted(signal);
} catch (error) {
rejectAbort?.(error);
}
};
signal?.addEventListener('abort', handleAbort, { once: true });
try {
const outcome = await Promise.race([
uiReady.then(() => 'ready' as const),
appDestroyed.then(() => 'destroyed' as const),
timeout,
aborted,
]);
if (outcome === 'destroyed') {
throw new Error('Dashboard is no longer available.');
}
} finally {
if (timer !== null) clearTimeout(timer);
signal?.removeEventListener('abort', handleAbort);
}
}
export async function applyWebMcpDashboardAction(
ctx: AppContext,
action: unknown,
options: AgentBusApplierOptions,
signal?: AbortSignal,
): Promise<DashboardActionResult> {
throwIfWebMcpAborted(signal);
if (ctx.isDestroyed) return APP_DESTROYED_RESULT;
// Keep the zod-backed agent-bus contract out of the eager dashboard entry.
const { applyAgentBusAction } = await import('./agent-bus-applier');View on GitHub (pinned to 9361220cc0)