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.

Source

Thrown at src/app/webmcp-dashboard.ts:89

  appDestroyed: Promise<void>,
  timeoutMs: number,
  target = 'UI',
): Promise<void> {
  let timer: ReturnType<typeof setTimeout> | null = null;
  const timeout = new Promise<never>((_, reject) => {
    timer = setTimeout(
      () => reject(new Error(`${target} did not initialise within ${timeoutMs}ms`)),
      timeoutMs,
    );
  });
  try {
    const outcome = await Promise.race([
      uiReady.then(() => 'ready' as const),
      appDestroyed.then(() => 'destroyed' as const),
      timeout,
    ]);
    if (outcome === 'destroyed') {
      throw new Error('Dashboard is no longer available.');
    }
  } finally {
    if (timer !== null) clearTimeout(timer);
  }
}

export async function applyWebMcpDashboardAction(
  ctx: AppContext,
  action: unknown,
  options: AgentBusApplierOptions,
): Promise<DashboardActionResult> {
  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');
  if (ctx.isDestroyed) return APP_DESTROYED_RESULT;
  const result = applyAgentBusAction(ctx, action, options);
  if (result.ok && result.actionType === 'set_view' && ctx.map) {

View on GitHub (pinned to eeab0a219f)

Solutions

  1. 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
  2. Delay tool registration or queue early tool calls until uiReady settles, so destroy-during-startup is handled by one place
  3. 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

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


AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21). Data as JSON: /api/errors/247b9eb8d32ecc57. Report an issue: GitHub.