Skip to content

chore(llm): cache-identity churn batch 2 — summary key over non-deduped pairs + volatile regime floats, forecast narrative hash over non-prompt values, whyMatters v5 fallback blind to v8 #4914

Description

@koala73

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions