{"record":{"id":"247b9eb8d32ecc57","repo":"koala73/worldmonitor","slug":"dashboard-is-no-longer-available","errorCode":null,"errorMessage":"Dashboard is no longer available.","messagePattern":"Dashboard is no longer available\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/app/webmcp-dashboard.ts","lineNumber":89,"sourceCode":"  appDestroyed: Promise<void>,\n  timeoutMs: number,\n  target = 'UI',\n): Promise<void> {\n  let timer: ReturnType<typeof setTimeout> | null = null;\n  const timeout = new Promise<never>((_, reject) => {\n    timer = setTimeout(\n      () => reject(new Error(`${target} did not initialise within ${timeoutMs}ms`)),\n      timeoutMs,\n    );\n  });\n  try {\n    const outcome = await Promise.race([\n      uiReady.then(() => 'ready' as const),\n      appDestroyed.then(() => 'destroyed' as const),\n      timeout,\n    ]);\n    if (outcome === 'destroyed') {\n      throw new Error('Dashboard is no longer available.');\n    }\n  } finally {\n    if (timer !== null) clearTimeout(timer);\n  }\n}\n\nexport async function applyWebMcpDashboardAction(\n  ctx: AppContext,\n  action: unknown,\n  options: AgentBusApplierOptions,\n): Promise<DashboardActionResult> {\n  if (ctx.isDestroyed) return APP_DESTROYED_RESULT;\n\n  // Keep the zod-backed agent-bus contract out of the eager dashboard entry.\n  const { applyAgentBusAction } = await import('./agent-bus-applier');\n  if (ctx.isDestroyed) return APP_DESTROYED_RESULT;\n  const result = applyAgentBusAction(ctx, action, options);\n  if (result.ok && result.actionType === 'set_view' && ctx.map) {","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/src/app/webmcp-dashboard.ts#L71-L107","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nawait waitForWebMcpUiReady(this.uiReady, this.appDestroyed, 10_000); // throws raw Error\n\n// after\ntry {\n  await waitForWebMcpUiReady(this.uiReady, this.appDestroyed, 10_000);\n} catch (error) {\n  return { ok: false, status: 'denied', reason: 'app_destroyed' } as const;\n}","handlingStrategy":"try-catch","validationCode":"if (appDestroyed.settled) return DESTROYED_RESULT; // if you track the promise, check before awaiting\nawait Promise.race([uiReady, appDestroyed, timeout]);","typeGuard":null,"tryCatchPattern":"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; }","preventionTips":["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"],"tags":["webmcp","agent-tools","lifecycle","startup","race-condition"],"backgroundTag":"stale-app-instance","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-23T16:17:53.355Z"}