You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to the 2026-07-05 spend investigation (#4892 fixed the dominant burner; #4911 fixed regional; #4909 fixed the shadow double). The same "volatile value folded into cache identity → TTL defeated → paid churn" class is still live in three places, plus one throttle gap. All refs verified against origin/main 2d2d659.
summarize-article cache key is computed over NON-deduped headline/body pairs.server/worldmonitor/news/v1/summarize-article.ts:156 derives the key via getCacheKey(headlines, …) BEFORE the pair-dedup at :171-193; src/utils/summary-cache-key.ts sorts + slices top-5 but never dedups, so the same unique story set with different duplicate composition mints different keys while producing the same deduped prompt. Fix in the shared builder (dedup identical pairs pre-slice) + CACHE_VERSION v6→v7.
Daily market brief folds raw volatile floats into the summary geoContext.src/services/daily-market-brief.ts:352-376 interpolates VIX to 0.1, FSI to 0.01, HY spread to 1bps, 2s/10s to 1bps, yields to 0.01%, sector top/worst to 0.1pp, and buildSummaryInputs (:334) index changes to 0.1pp — the whole string becomes :g<hash> in the summary cache key, so every 5-min quote/regime tick and every user mints a fresh key against a 24h-class TTL. Quantize at source (prompt and key stay consistent): index moves to 0.25pp, VIX to 1pt, HY to 10bps, 2s10s to 5bps, yields to 0.1%, sector to 0.5pp.
Forecast narrative cache hash covers values the prompt never sees, at 1h TTL.scripts/seed-forecasts.mjs:14629 (buildCacheHash) hashes raw probability floats, all newsContext entries, and cascade probability floats, while buildUserPrompt renders integer-rounded percentages and only the top-3 headlines — keys churn hourly even when the prompt is byte-identical, and the 3600s TTL (:15081) expires between hourly runs anyway. Fix: key over the exact prompt text (the chore(llm): regional hygiene — narrative prompt-hash cache + brief retry-budget cap #4911 prompt-hash pattern, system+user) and extend TTL to 24h.
whyMatters cron fallback is blind to the endpoint's cache.scripts/lib/brief-llm.mjs:171 (v5) and api/internal/brief-why-matters.ts:398 (v8) share hashBriefStory but are disjoint namespaces: when the analyst endpoint call fails transiently, the cron pays a direct-Gemini generation for a story that may already have a valid v8 envelope. Fix: fallback path reads the v8 envelope (read-only, bounds-validated) before paying.
Tracked separately (product-behavioral, own PR): the digest generator runs the paid synthesis chain for every eligible user on every 30-min tick regardless of isDue (scripts/seed-digest-notifications.mjs:1715 compose loop; due only picks the winner) with a pool-hash key that churns as the accumulator grows — needs a non-due synthesis cooldown that preserves the dashboard-refresh contract.
Follow-up to the 2026-07-05 spend investigation (#4892 fixed the dominant burner; #4911 fixed regional; #4909 fixed the shadow double). The same "volatile value folded into cache identity → TTL defeated → paid churn" class is still live in three places, plus one throttle gap. All refs verified against origin/main 2d2d659.
summarize-article cache key is computed over NON-deduped headline/body pairs.
server/worldmonitor/news/v1/summarize-article.ts:156derives the key viagetCacheKey(headlines, …)BEFORE the pair-dedup at :171-193;src/utils/summary-cache-key.tssorts + slices top-5 but never dedups, so the same unique story set with different duplicate composition mints different keys while producing the same deduped prompt. Fix in the shared builder (dedup identical pairs pre-slice) + CACHE_VERSION v6→v7.Daily market brief folds raw volatile floats into the summary geoContext.
src/services/daily-market-brief.ts:352-376interpolates VIX to 0.1, FSI to 0.01, HY spread to 1bps, 2s/10s to 1bps, yields to 0.01%, sector top/worst to 0.1pp, andbuildSummaryInputs(:334) index changes to 0.1pp — the whole string becomes:g<hash>in the summary cache key, so every 5-min quote/regime tick and every user mints a fresh key against a 24h-class TTL. Quantize at source (prompt and key stay consistent): index moves to 0.25pp, VIX to 1pt, HY to 10bps, 2s10s to 5bps, yields to 0.1%, sector to 0.5pp.Forecast narrative cache hash covers values the prompt never sees, at 1h TTL.
scripts/seed-forecasts.mjs:14629(buildCacheHash) hashes raw probability floats, all newsContext entries, and cascade probability floats, whilebuildUserPromptrenders integer-rounded percentages and only the top-3 headlines — keys churn hourly even when the prompt is byte-identical, and the 3600s TTL (:15081) expires between hourly runs anyway. Fix: key over the exact prompt text (the chore(llm): regional hygiene — narrative prompt-hash cache + brief retry-budget cap #4911 prompt-hash pattern, system+user) and extend TTL to 24h.whyMatters cron fallback is blind to the endpoint's cache.
scripts/lib/brief-llm.mjs:171(v5) andapi/internal/brief-why-matters.ts:398(v8) sharehashBriefStorybut are disjoint namespaces: when the analyst endpoint call fails transiently, the cron pays a direct-Gemini generation for a story that may already have a valid v8 envelope. Fix: fallback path reads the v8 envelope (read-only, bounds-validated) before paying.Tracked separately (product-behavioral, own PR): the digest generator runs the paid synthesis chain for every eligible user on every 30-min tick regardless of
isDue(scripts/seed-digest-notifications.mjs:1715compose loop;dueonly picks the winner) with a pool-hash key that churns as the accumulator grows — needs a non-due synthesis cooldown that preserves the dashboard-refresh contract.