koala73/worldmonitor · error · DashboardBindingError

app_destroyed

app_destroyed

Error message

Dashboard is no longer available.

What it means

DashboardBindingError 'app_destroyed' thrown by getWebMcpDashboardContext() when ctx.isDestroyed is true. The context snapshot builder must never read UI state from a destroyed app, so it refuses before touching ctx.panels or ctx.map. This is the snapshot-getter sibling of the tool-binding guards in App.ts.

Source

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

    ...result,
    ok: false,
    status: 'denied',
    reason,
    message: reason === 'viewport_superseded'
      ? 'Map movement was superseded by a newer viewport action.'
      : reason === 'renderer_changed'
        ? 'Map renderer changed before the movement completed.'
        : 'Map movement was interrupted before it completed.',
    targets: result.targets.map((target) => ({ ...target, status: 'denied', reason })),
  };
}

export function getWebMcpDashboardContext(
  ctx: AppContext,
  variant: string,
): DashboardContextSnapshot {
  if (ctx.isDestroyed) {
    throw new DashboardBindingError('app_destroyed', 'Dashboard is no longer available.');
  }
  if (!ctx.map) {
    throw new DashboardBindingError('map_unavailable', 'Map is not available.');
  }

  const mapState = ctx.map.getState();
  const center = ctx.map.getCenter();

  return {
    variant,
    map: {
      view: mapState.view,
      center,
      zoom: mapState.zoom,
      timeRange: mapState.timeRange,
      enabledLayers: Object.entries(mapState.layers)
        .filter(([, enabled]) => enabled === true)
        .map(([layer]) => layer),

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Check ctx.isDestroyed before requesting a context snapshot and skip/queue the call
  2. Catch DashboardBindingError, branch on code 'app_destroyed', and drop or re-derive the tool bindings against a new App instance
  3. Unregister WebMCP tools in destroy() (the controller stored from registerWebMcpTools exists for exactly this) so agents cannot reach a dead context

Example fix

// before
const snapshot = getWebMcpDashboardContext(ctx, variant); // throws on destroyed ctx

// after
if (ctx.isDestroyed) {
  return { ok: false, status: 'denied', reason: 'app_destroyed' } as const;
}
const snapshot = getWebMcpDashboardContext(ctx, variant);
Defensive patterns

Strategy: type-guard

Validate before calling

function isContextUsable(ctx: AppContext): boolean {
  return !ctx.isDestroyed;
}
if (!isContextUsable(ctx)) return { ok: false, status: 'denied', reason: 'app_destroyed' };

Type guard

function hasLiveAppContext(ctx: AppContext): boolean { return !ctx.isDestroyed; }

Try / catch

try { return getWebMcpDashboardContext(ctx, variant); } catch (e) { if (e instanceof DashboardBindingError && e.code === 'app_destroyed') return DESTROYED_SNAPSHOT; throw e; }

Prevention

When it happens

Trigger: An agent calls get_dashboard_context after the App was destroyed — e.g. the tool call raced page unload, HMR teardown, or explicit app.destroy(). The first thing the function does is refuse dead contexts; note the very next guard throws 'map_unavailable' for a live app without a map, so hitting this message specifically means the app itself is gone.

Common situations: Agent sessions that outlive the dashboard (client keeps calling tools after the tab started unloading); tests exercising context snapshots against a destroyed fixture; stale tool registrations surviving an HMR boundary.

Related errors


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