{"record":{"id":"11127de37783f2ad","repo":"koala73/worldmonitor","slug":"no-hotspot-escalation-input-feeds-are-available","errorCode":null,"errorMessage":"No hotspot-escalation input feeds are available","messagePattern":"No hotspot-escalation input feeds are available","errorType":"exception","errorClass":"McpSourceUnavailableError","httpStatus":null,"severity":"error","filePath":"api/mcp/registry/analysis-tools.ts","lineNumber":178,"sourceCode":"    items: { type: 'string' },\n    description: 'Required cache keys that were missing or unreadable; their contribution is not treated as quiet.',\n  },\n  failed_inputs: {\n    type: 'array',\n    items: { type: 'string' },\n    description: 'Subset of unavailable_inputs whose Redis read failed rather than returning a genuine miss.',\n  },\n} as const;\n\ntype AnalysisFreshness = Awaited<ReturnType<typeof readCachesWithFreshness>>['freshness'];\n\nfunction requireAnyInput(\n  payloads: unknown[],\n  freshness: AnalysisFreshness,\n  message: string,\n): void {\n  if (payloads.every((value) => value === null)) {\n    throw new McpSourceUnavailableError(\n      message,\n      freshness.unavailable_inputs,\n      freshness.failed_inputs,\n    );\n  }\n}\n\nfunction resolveLimit(raw: unknown, fallback: number): number {\n  if (raw === undefined || raw === null) return fallback;\n  const parsed = Math.round(Number(raw));\n  if (!Number.isFinite(parsed)) return fallback;\n  if (parsed <= 0) return Number.POSITIVE_INFINITY;\n  return parsed;\n}\n\nexport const ANALYSIS_TOOLS: ToolDef[] = [\n  {\n    name: 'get_signal_convergence',","sourceCodeStart":160,"sourceCodeEnd":196,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/api/mcp/registry/analysis-tools.ts#L160-L196","documentation":"Thrown by requireAnyInput() inside the get_hotspot_escalation MCP tool. The tool reads five feeds (news insights, CII risk scores, military flights, unrest events, earthquakes) to compute escalation scores for the 29 curated hotspots. If all five are null, no score can be computed for any hotspot, so it throws McpSourceUnavailableError. Note that hotspot_id validation happens BEFORE the cache read, so this error only fires when the hotspot_id is valid or omitted — an unknown hotspot_id returns an error envelope, never this throw.","triggerScenarios":"Calling get_hotspot_escalation (with or without hotspot_id) when news:insights:v1, intelligence:risk-scores:live, military:flights:v1, unrest:events:v1, and seismology:earthquakes:v1 are all null in Redis simultaneously. This is a total feed outage across news, risk, military, unrest, and seismology domains.","commonSituations":"Cold start before seeders populate the five caches; Redis flush or failover; a systemic seeder pipeline outage affecting multiple domain seed jobs at once; env misconfiguration where the edge function reads from a different (empty) Redis instance than the seeders write to.","solutions":["Verify the five seed-meta keys exist via GET /api/health: seed-meta:news:insights, seed-meta:intelligence:risk-scores, seed-meta:military:flights, seed-meta:unrest:events, seed-meta:seismology:earthquakes.","Retry after a short delay — if some feeds are mid-refresh the next call may find at least one non-null payload.","If the outage is isolated to one or two feeds (not all five), this error should NOT fire; investigate why readCachesWithFreshness returned null for the others too.","Confirm the seeder jobs are running on schedule (Railway cron) and writing seed-meta:* markers."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Check the five hotspot feeds before calling get_hotspot_escalation\nconst health = await fetch('/api/health').then(r => r.json());\nconst feeds = ['news:insights','intelligence:risk-scores','military:flights','unrest:events','seismology:earthquakes'];\nconst live = feeds.filter(k => health.seedMeta?.[`seed-meta:${k}`]);\nif (live.length === 0) {\n  throw new Error('All hotspot-escalation feeds unseeded; retry after seeder runs');\n}","typeGuard":"function isMcpSourceUnavailableError(e: unknown): e is { unavailableInputs: string[]; failedInputs: string[] } & Error {\n  return e instanceof Error && (e as any).name === 'McpSourceUnavailableError';\n}","tryCatchPattern":"try {\n  const result = await callMcpTool('get_hotspot_escalation', { hotspot_id: 'ukraine' });\n} catch (e) {\n  if (isMcpSourceUnavailableError(e)) {\n    // All five feeds null — wait for seeder and retry\n    await backoffRetry(60_000);\n  } else throw e;\n}","preventionTips":["Validate hotspot_id against the known curated list first (avoids the separate unknown-id error).","Monitor the five feed seed-meta keys; this error means ALL are down simultaneously.","Cache the last successful hotspot scores as a degraded fallback."],"tags":["mcp","redis","source-unavailable","cache-miss","hotspot-escalation"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}