fix(forecast): stop false SEED_ERROR when market_implications is starved of the shared LLM run budget (#4978)#4980
Merged
Conversation
…ved of the shared LLM run budget (#4978) market_implications is the LAST forecast LLM stage (afterPublish) and shares the single 150s run budget with every upstream stage. When upstream stages are slow (e.g. deepseek-v4-flash breaching its 25s call timeout on combined/ scenario, #4944), they drain that budget before this tail stage runs; callForecastLLM then throws a budget error and returns null. Pre-fix the caller treated that starve identically to a real LLM failure and wrote a status:'error' seed-meta, so /api/health flipped to SEED_ERROR for benign, self-healing resource contention (observed 2026-07-06 14:03; self-healed 14:15). Two changes: - Distinguish a budget-starve from a real failure. On starve (pre-call guard, or a null result with the run budget now <=0), preserve last-good WITHOUT rewriting seed-meta.fetchedAt, so age-based STALE_SEED (maxStaleMin=120) still escalates if the starve persists past 2h (gpsjam preserve-last-good design). Genuine provider failures with budget remaining still surface SEED_ERROR. - Reorder: run market_implications BEFORE the best-effort telemetry in afterPublish (history + deep-forecast snapshots, ~20s R2 trace export) so that wall-clock can't push the tail stage past the run deadline. This recovers the ~20s that pushed it over the 150s deadline in the failing run. Root trigger (deepseek-v4-flash latency draining the shared budget) is #4944 territory; a complementary combined-stage groq fallback is a 1-line Railway env change (FORECAST_LLM_COMBINED_PROVIDER_ORDER=openrouter,groq), left out of code to respect the intentional #4944 pin. Tests: tests/market-implications-budget-starve.test.mjs (starve preserves last-good + no error meta; genuine failure still errors) + an afterPublish ordering guard in market-implications-seed-health.test.mjs. Failing-test-first per Bug Fix Protocol. Claude-Session: https://claude.ai/code/session_019uhnqAKEHTws3QMaUvq58N
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
Address PR #4980 review feedback by keeping provider failures distinct from run-budget starvation and restoring stale OK market-implications meta from last-good payloads during starved runs. Also updates the budget-starve tests so the provider-failure regression performs a real provider request.
…vider timeout + harden classification (#4980 review) Addresses the ce-code-review findings on #4980: - #1/#4: raise MARKET_IMPLICATIONS_MIN_RUN_BUDGET_MS 20_000 -> 30_000 (= max provider.timeout 25s + FORECAST_LLM_STAGE_BUDGET_GUARD_MS 5s). At 20s the pre-call guard admitted calls that were then timeout-CAPPED below the provider's own timeout, which is indistinguishable from a genuinely hung provider: a real openrouter timeout in the [20s,30s) band was misclassified as a benign budget starve and its SEED_ERROR suppressed, and 20-25s calls were guaranteed to abort mid-flight (~15s wasted). Admitting only at >=30s gives the provider its full window, so any timeout is attributable and a genuine failure still surfaces SEED_ERROR. - #2: getRemainingForecastLlmBudgetMs now delegates the run-remaining calc to getRemainingForecastLlmRunBudgetMs (was a verbatim duplicate formula). - #3: document the failure-reason classification invariant on the sawProviderFailure / sawBudgetCappedTimeout declarations and the budget-capped-timeout heuristic (comments only, no behavior change). - #5: add tests for the 20-30s guard skip (locks the 30s threshold), the retained mid-call budget_exhausted preserve branch, and its provider_failed symmetry (via the __setForecastLlmCallOverrideForTests seam). - #6: test 1 now sets a real provider key + transport counter so its llmCalls===0 tripwire exercises the guard instead of the !apiKey short-circuit; removed the now-unused budgetStarveFetch helper. test 4 deadline bumped 25s -> 35s to keep passing the new 30s guard. Tests: 605 pass across all seed-forecasts.mjs consumers; biome + unicode clean. Claude-Session: https://claude.ai/code/session_017odw3Pf9ue8RZzxYAQ37P8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4978.
Problem
/api/healthintermittently reportsmarketImplications: SEED_ERROR. The market-implications LLM stage is not failing on its own merits — it is being starved of the shared 150s forecast LLM run budget and then mislabels that starve as a producer failure.market_implicationsruns LAST (inafterPublish) and shares one run budget (FORECAST_LLM_RUN_BUDGET_MS = 150_000) with every upstream LLM stage. Whencombined/scenariobreach the 25sdeepseek/deepseek-v4-flashcall timeout (the #4944 model), they drain that budget before the tail stage runs;callForecastLLMthrows a budget error and returnsnull. Pre-fix the caller treated thatnullidentically to a real failure and wrote astatus:'error'seed-meta → healthSEED_ERROR. It self-heals next tick whenever upstream returns under the timeout.Evidence (one seed-forecasts container, 2 consecutive runs, 2026-07-06):
combined+scenariotimed out →[LLM:market_implications] budget exhausted after 1ms→writing error seed-meta.Changes
nullresult with the run budget now<=0is treated the same. On a starve we preserve last-good WITHOUT rewritingseed-meta.fetchedAt, so age-basedSTALE_SEED(maxStaleMin=120) still escalates if the starve persists past 2h — the gpsjam preserve-last-good design. Genuine provider failures (budget remaining) still surfaceSEED_ERROR.market_implicationsbefore the best-effort telemetry inafterPublish(history + deep-forecast snapshots, ~20s R2 trace export) so their wall-clock can't push the tail stage past the run deadline. Recovers the ~20s that tipped the failing run over the 150s deadline. Trace export is unaffected (verified byforecast-trace-export.test.mjs).Not in this PR (env follow-up)
The root trigger — deepseek-v4-flash latency draining the shared budget — is #4944 territory. A complementary combined-stage groq fallback is a 1-line Railway env change (
FORECAST_LLM_COMBINED_PROVIDER_ORDER=openrouter,groq), deliberately left out of code to respect the intentional #4944 provider pin. Recommend applying it so a combined timeout stops wasting ~25s and degrading forecast narratives tofallbackNarratives.Test plan
tests/market-implications-budget-starve.test.mjs(new): starve →status:'ok'preserved,fetchedAtuntouched, no stage-cache write; genuine failure (budget remaining) → stillSEED_ERROR. Failing-test-first per Bug Fix Protocol (failed'error' !== 'ok'pre-fix).market-implications-seed-health.test.mjs.node --testacross market-implications + forecast suites: 336 pass.https://claude.ai/code/session_019uhnqAKEHTws3QMaUvq58N