{"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":197,"sourceCode":"    rejectAbort = reject;\n  });\n  const handleAbort = (): void => {\n    try {\n      throwIfWebMcpAborted(signal);\n    } catch (error) {\n      rejectAbort?.(error);\n    }\n  };\n  signal?.addEventListener('abort', handleAbort, { once: true });\n  try {\n    const outcome = await Promise.race([\n      uiReady.then(() => 'ready' as const),\n      appDestroyed.then(() => 'destroyed' as const),\n      timeout,\n      aborted,\n    ]);\n    if (outcome === 'destroyed') {\n      throw new Error('Dashboard is no longer available.');\n    }\n  } finally {\n    if (timer !== null) clearTimeout(timer);\n    signal?.removeEventListener('abort', handleAbort);\n  }\n}\n\nexport async function applyWebMcpDashboardAction(\n  ctx: AppContext,\n  action: unknown,\n  options: AgentBusApplierOptions,\n  signal?: AbortSignal,\n): Promise<DashboardActionResult> {\n  throwIfWebMcpAborted(signal);\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');","sourceCodeStart":179,"sourceCodeEnd":215,"githubUrl":"https://github.com/koala73/worldmonitor/blob/9361220cc013571781071f0206e4d80fd14b2f7f/src/app/webmcp-dashboard.ts#L179-L215","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":"9361220cc013571781071f0206e4d80fd14b2f7f","analyzedAt":"2026-08-27T19:53:08.521Z","contentChangedAt":"2026-08-27T19:53:08.521Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}