{"record":{"id":"e4db497b3ab3fb3b","repo":"koala73/worldmonitor","slug":"no-convergence-input-feeds-are-available","errorCode":null,"errorMessage":"No convergence input feeds are available","messagePattern":"No convergence input feeds are available","errorType":"exception","errorClass":"McpSourceUnavailableError","httpStatus":null,"severity":"error","filePath":"api/mcp/registry/analysis-tools.ts","lineNumber":179,"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\n// Keep the analysis schemas aligned with cacheEnvelope(). Content age is not a\n// universal rule: evaluateFreshness() applies it only to checks that explicitly\n// declare honorContentAge.","sourceCodeStart":161,"sourceCodeEnd":197,"githubUrl":"https://github.com/koala73/worldmonitor/blob/9361220cc013571781071f0206e4d80fd14b2f7f/api/mcp/registry/analysis-tools.ts#L161-L197","documentation":"McpSourceUnavailableError thrown by requireAnyInput (api/mcp/registry/analysis-tools.ts:173) for the convergence analysis tool: every input cache payload read via readCachesWithFreshness came back null, so no live input feed exists to compute convergence from. The error preserves freshness.unavailable_inputs (genuine cache misses) and freshness.failed_inputs (Redis reads that errored) so dispatch can surface them in JSON-RPC error.data and the caller can distinguish a retryable outage from an unseeded dataset.","triggerScenarios":"Calling the convergence analysis tool while all of its input cache keys are null — upstream producer pipelines (news/analysis writers) stopped publishing, keys expired between producer runs, Redis read failures, or a deployment where the analysis seeds have never run.","commonSituations":"New environment without seed data. Producer cron/edge job failure upstream of MCP. Upstash degradation turning reads into nulls. Post-deploy window before the first producer cycle completes.","solutions":["Inspect error.data.unavailable_inputs vs failed_inputs: failed_inputs present means Redis read errors (check Upstash); all-miss means the producer has not written","Check /api/health for the convergence input keys' freshness/seed-meta markers","Retry after the producer's next publish cycle if it is a timing gap","If keys are absent and should exist, run the corresponding seed script or restart the producer job"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Pre-check the convergence inputs' freshness on the health surface\nconst health = await fetch('https://<host>/api/health').then(r => r.json());\nconst inputsReady = convergenceInputKeys.every(k => health?.datasets?.[k]?.fresh !== false);","typeGuard":"function isSourceUnavailable(e) {\n  return typeof e === 'object' && e !== null\n    && (e.name === 'McpSourceUnavailableError'\n      || (Array.isArray(e.data?.unavailable_inputs) && Array.isArray(e.data?.failed_inputs)));\n}","tryCatchPattern":"try {\n  const r = await client.callTool('get_convergence_analysis', args);\n} catch (e) {\n  if (isSourceUnavailable(e)) {\n    const { unavailable_inputs = [], failed_inputs = [] } = e.data ?? {};\n    if (failed_inputs.length > 0) return retryWithBackoff(call, 3);   // Redis read failures — retryable\n    scheduleRetry(producerCadenceMs);                                   // genuine miss — wait for producer\n  }\n  throw e;\n}","preventionTips":["Read error.data.unavailable_inputs vs failed_inputs before choosing retry vs wait","Gate analysis workflows on /api/health freshness for their input keys","If failed_inputs persists across retries, escalate as an Upstash incident, not a data gap"],"tags":["mcp","analysis","redis","data-source","convergence"],"backgroundTag":"data-source-unavailable","analyzedSha":"9361220cc013571781071f0206e4d80fd14b2f7f","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}