Phase 3 PR2: Weekly regional briefs (LLM seeder + RPC)#2989
Conversation
Phase 3 PR2 of the Regional Intelligence Model. Adds LLM-powered weekly intelligence briefs per region, completing the core feature set. ## New seeder: scripts/seed-regional-briefs.mjs Standalone weekly cron script (not part of the 6h derived-signals bundle). For each non-global region: 1. Read the latest snapshot via two-hop Redis read 2. Read recent regime transitions from the history log (#2981) 3. Call the LLM once per region with regime trajectory + balance + triggers + narrative context 4. Write structured brief to intelligence:regional-briefs:v1:weekly:{region} with 8-day TTL (survives one missed weekly run) Reuses the same injectable-callLlm + parse-validation + provider-chain pattern from narrative.mjs and weekly-brief.mjs. ## New module: scripts/regional-snapshot/weekly-brief.mjs generateWeeklyBrief(region, snapshot, transitions, opts?) -> { region_id, generated_at, period_start, period_end, situation_recap, regime_trajectory, key_developments[], risk_outlook, provider, model } buildBriefPrompt() — pure prompt builder parseBriefJson() — JSON parser with prose-extraction fallback emptyBrief() — canonical empty shape Global region is skipped. Provider chain: Groq -> OpenRouter. Validate callback ensures only parseable responses pass (narrative.mjs PR #2960 review fix pattern). ## Proto + RPC: GetRegionalBrief proto/worldmonitor/intelligence/v1/get_regional_brief.proto - GetRegionalBriefRequest { region_id } - GetRegionalBriefResponse { brief: RegionalBrief } - RegionalBrief { region_id, generated_at, period_start, period_end, situation_recap, regime_trajectory, key_developments[], risk_outlook, provider, model } ## Server handler server/worldmonitor/intelligence/v1/get-regional-brief.ts Simple getCachedJson read + adaptBrief snake->camel adapter. Returns upstreamUnavailable: true on Redis failure so the gateway skips caching (matching the get-regime-history pattern from #2981). ## Premium gating + cache tier src/shared/premium-paths.ts + server/gateway.ts RPC_CACHE_TIER ## Tests — 27 new unit tests buildBriefPrompt (5): region/balance/transitions/narrative rendered, empty transitions handled, missing fields tolerated parseBriefJson (5): valid JSON, garbage, all-empty, cap at 5, prose extraction generateWeeklyBrief (6): success, global skip, LLM fail, garbage, exception, period_start/end delta emptyBrief (2): region_id + empty fields handler (4): key prefix, adapter export, upstreamUnavailable, registration security (2): premium path + cache tier proto (3): RPC declared, import wired, RegionalBrief fields ## Verification - npm run test:data: 4651/4651 pass - npm run typecheck + typecheck:api: clean - biome lint: clean
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Greptile SummaryThis PR adds LLM-powered weekly intelligence briefs per region: a new seeder ( Confidence Score: 4/5The P1 TTL bug in the seeder must be fixed before deploying — briefs would accumulate in Redis indefinitely without it. One P1 defect: scripts/seed-regional-briefs.mjs — Important Files Changed
Sequence DiagramsequenceDiagram
participant Cron as Weekly Cron (Railway)
participant Seeder as seed-regional-briefs.mjs
participant Redis as Redis (Upstash)
participant LLM as LLM (Groq / OpenRouter)
participant Gateway as Vercel Edge Gateway
participant Handler as get-regional-brief.ts
participant Client as Client (Premium)
Cron->>Seeder: trigger weekly run
loop For each non-global region
Seeder->>Redis: GET intelligence:snapshot:v1:{region}:latest
Redis-->>Seeder: snapshot_id
Seeder->>Redis: GET intelligence:snapshot-by-id:v1:{snapshot_id}
Redis-->>Seeder: snapshot JSON
Seeder->>Redis: LRANGE intelligence:regime-history:v1:{region} 0 49
Redis-->>Seeder: regime transition entries
Seeder->>LLM: POST chat completions (brief prompt)
LLM-->>Seeder: JSON brief
Seeder->>Redis: SET intelligence:regional-briefs:v1:weekly:{region} (8d TTL)
end
Seeder->>Redis: writeExtraKeyWithMeta (seed-meta, summary)
Client->>Gateway: GET /api/intelligence/v1/get-regional-brief?region_id=mena
Gateway->>Gateway: Auth check (PREMIUM_RPC_PATHS)
Gateway->>Handler: getRegionalBrief(req)
Handler->>Redis: getCachedJson(raw=true) intelligence:regional-briefs:v1:weekly:mena
Redis-->>Handler: brief JSON (or null)
Handler-->>Gateway: { brief } or { upstreamUnavailable: true }
Gateway-->>Client: 200 JSON (slow-browser cache)
Reviews (1): Last reviewed commit: "feat(intelligence): weekly regional brie..." | Re-trigger Greptile |
| const key = `${BRIEF_KEY_PREFIX}${regionId}`; | ||
| const payload = JSON.stringify(brief); | ||
| try { | ||
| const resp = await fetch(`${url}/set/${encodeURIComponent(key)}/${encodeURIComponent(payload)}?EX=${BRIEF_TTL}`, { |
There was a problem hiding this comment.
TTL silently not applied — briefs will never expire
The Upstash REST API passes Redis command options as path segments (/EX/<seconds>), not as query parameters. Sending ?EX=${BRIEF_TTL} appends it to the query string, which Upstash ignores; the SET executes without any TTL, so every brief will persist indefinitely and Redis storage grows unboundedly across weekly runs.
Compare with the canonical pattern in server/_shared/redis.ts (setCachedJson):
/set/${key}/${value}/EX/${ttlSeconds} ← correct path-segment form
/set/${key}/${value}?EX=${ttl} ← current code — query string ignored
| const resp = await fetch(`${url}/set/${encodeURIComponent(key)}/${encodeURIComponent(payload)}?EX=${BRIEF_TTL}`, { | |
| const resp = await fetch(`${url}/set/${encodeURIComponent(key)}/${encodeURIComponent(payload)}/EX/${BRIEF_TTL}`, { |
Alternatively, use the JSON-body pipeline form that writeExtraKey in _seed-utils.mjs already uses:
body: JSON.stringify(['SET', key, payload, 'EX', BRIEF_TTL]),| if (failed === 0 && generated > 0) { | ||
| await writeExtraKeyWithMeta( | ||
| `intelligence:regional-briefs:summary:v1`, | ||
| { generatedAt: Date.now(), regionsGenerated: generated }, | ||
| BRIEF_TTL, | ||
| generated, | ||
| `seed-meta:${SEED_META_KEY}`, | ||
| BRIEF_TTL, | ||
| ); | ||
| } |
There was a problem hiding this comment.
seed-meta not refreshed when all regions are skipped
writeExtraKeyWithMeta is guarded by failed === 0 && generated > 0. If every region is skipped because no snapshot exists yet (or LLM fails across the board), generated remains 0 and the seed-meta key is never written. The AGENTS.md convention requires every seed script to write seed-meta:<key> on each run so health monitoring can confirm the seeder is executing — a perpetually absent seed-meta is indistinguishable from a seeder that has never run.
Consider writing the meta unconditionally (or at minimum when generated === 0 && failed === 0), carrying regionsSkipped in the payload so health tooling can distinguish "ran but nothing to do" from "never ran".
| : []; | ||
| const risk_outlook = typeof p.risk_outlook === 'string' ? p.risk_outlook.trim() : ''; | ||
|
|
||
| const hasContent = situation_recap.length > 0 || key_developments.length > 0; |
There was a problem hiding this comment.
Validate gate and seeder skip condition are inconsistent
parseBriefJson marks a response valid when situation_recap.length > 0 || key_developments.length > 0. A response with non-empty key_developments but an empty situation_recap therefore passes the validate callback in callLlmDefault, meaning the provider chain does not retry — it accepts the response and returns it. generateWeeklyBrief then returns that brief, but seed-regional-briefs.mjs immediately discards it (if (!brief.situation_recap) { skipped++; continue; }).
The net effect is that an LLM response that omits situation_recap exhausts the retry loop (since validation says it's fine) yet produces nothing useful. Tighten the validation to require situation_recap:
| const hasContent = situation_recap.length > 0 || key_developments.length > 0; | |
| const hasContent = situation_recap.length > 0; |
P2 #1 — no consumer surface for GetRegionalBrief Acknowledged. The consumer is the RegionalIntelligenceBoard panel, which will call GetRegionalBrief and render a weekly brief block. This wiring is Phase 3 PR3 (UI) scope — the RPC + Redis key are the delivery mechanism, not the end surface. No code change in this commit; the RPC is ready for the panel to consume. P2 #2 — readRecentTransitions collapses failure to [] readRecentTransitions returned [] on Redis/network failure, which is indistinguishable from a genuinely quiet week. The LLM then generates a brief claiming "no regime transitions" when in reality the upstream is down — fabricating false input. Fix: return null on failure. The seeder skips the region with a clear log message when transitions is null, so the brief is never written with unreliable input. Empty array [] now only means genuinely no transitions in the 7-day window. P2 #3 — parseBriefJson accepts briefs the seeder rejects parseBriefJson treated non-empty key_developments as valid even if situation_recap was empty. The seeder gate only writes when brief.situation_recap is truthy. That mismatch means the validator pass + provider-fallback logic could accept a response that the seeder then silently drops. Fix: require situation_recap in parseBriefJson for valid=true, matching the seeder gate. Now both checks agree on what constitutes a usable brief, and the provider-fallback chain correctly falls through when a provider returns a brief with developments but no recap.
…ptile P1+P2 on #2989) P1 — TTL silently not applied (briefs never expire) Upstash REST ignores query-string SET options (?EX=N). The correct form is path-segment: /set/{key}/{value}/EX/{seconds}. Without this fix every brief persists indefinitely and Redis storage grows unboundedly across weekly runs. P2 — seed-meta not written when all regions skipped writeExtraKeyWithMeta was gated on generated > 0. If every region was skipped (no snapshot yet, or LLM failed), seed-meta was never written, making the seeder indistinguishable from "never ran" in health tooling. Now writes seed-meta whenever failed === 0, carrying regionsSkipped count. P2 #3 (validate gate) — already fixed in previous commit (parseBriefJson now requires situation_recap for valid=true).
…STANDALONE_KEYS (review P2 on #2989)
…cycle (review P2 on #2989)
…e-gated health (review P2s on #2989) P2 #1 — false upstreamUnavailable before first seed getCachedJson returns null for both "key missing" and "Redis failed", so the handler was advertising an outage for every region before the first weekly seed ran. Switched to getRawJson (throws on Redis errors) so null = genuinely missing key → clean empty 200, and thrown error = upstream failure → upstreamUnavailable: true for gateway no-store. P2 #2 — partial run hides coverage loss in health The seed-meta was written with generated count even if only 1 of 7 regions produced a brief. /api/health treats any positive recordCount as healthy, so broad regional failure was invisible to operators. Fix: recordCount is set to 0 when generated < ceil(expectedRegions/2). This makes /api/health report EMPTY_DATA for severely partial runs while still writing seed-meta (so the seeder is confirmed to have run). coverageOk flag in the summary payload lets operators drill into the exact coverage state.
…erage (#4164) * fix(regional-briefs): bypass weekly cooldown when last run failed coverage seed-bundle-regional gates the weekly brief sub-run behind a 6.5-day cooldown read from seed-meta:intelligence:regional-briefs. When a briefs run fails coverage (e.g. a transient OpenRouter-credits outage makes every region return an empty brief → skipped, failed===0), seed-regional-briefs deliberately writes recordCount=0 (+ coverageOk:false) so /api/health flips to EMPTY_DATA instead of hiding partial failure (PR #2989). The trap: that EMPTY_DATA crit then persists for the remainder of the ~6.5-day cooldown with no retry — even though re-running once credits are restored would clear it on the next 6h tick. Live incident 2026-06-06: "[bundle] briefs: last run 1.6 days ago, skipping (cooldown 6.5d)". Fix: in shouldRunBriefs, bypass the cooldown when the last run's recordCount===0. recordCount lives in the bare-shape seed-meta and is exactly the field that drives /api/health's EMPTY_DATA verdict, so it is the authoritative "last run failed coverage" signal — a successful run writes a positive count and keeps the normal weekly cooldown. This turns a transient failure into a self-healing retry-next-tick instead of a multi-day stuck crit. Coverage logic in seed-regional-briefs is unchanged. Adds an isMain guard + exports shouldRunBriefs so the bypass can be behaviorally tested without executing the bundle on import. * test(regional-briefs): loosen cooldown source guard --------- Co-authored-by: e <[email protected]>
…try-budget cap (#4911) * chore(llm): regional hygiene — narrative prompt-hash cache + brief retry-budget cap Two of the #4896 waste patterns: 1. Narrative-before-dedup: generateRegionalNarrative fired its ~900-token call before any content-identity check — byte-identical world state (7 regions x 4 bundle runs/day) regenerated the same narrative every 6h, and same-15min-bucket re-runs burned it just for persistSnapshot's idempotency guard to discard the result. The generator now caches the parsed narrative under a hash of the exact prompt (whose only volatile input is a day-granular date), 24h TTL, injectable for tests, best-effort on Redis failure. Only VALID parsed narratives are cached. 2. Coverage-fail retry storm: the #2989 cooldown bypass re-ran ALL region briefs on every 6h tick for as long as an outage lasted (~28 LLM calls/day, timed exactly to provider incidents) because each failed attempt refreshes seed-meta fetchedAt. A rolling 24h retry budget (INCR+EXPIRE NX, 2 bypasses/window) keeps the fast transient self-heal — first retry on the next tick, one more after — then pauses until the window expires. Fails OPEN on Redis errors so a hiccup can never block self-healing; the scheduled weekly cadence never consults the budget. Refs #4896 Claude-Session: https://claude.ai/code/session_01YTm4GsLG2M7ZuaHF9MpZih * fix(narrative-cache): default cache must not process.exit when Redis creds are absent getRedisCredentials() exits the process on missing env — the default narrative cache killed the CI unit runner (and any credless local run) the moment generateRegionalNarrative touched it without an injected cache. Read env directly and no-op without creds: no cache, generation proceeds exactly as before the cache existed. Caught by the unit job on this PR; reproduced locally with UPSTASH_* unset. Claude-Session: https://claude.ai/code/session_01YTm4GsLG2M7ZuaHF9MpZih
Summary
Phase 3 PR2 of the Regional Intelligence Model. Adds LLM-powered weekly intelligence briefs per region, completing the core server-side feature set. No UI changes.
What landed
New seeder: `scripts/seed-regional-briefs.mjs`
Standalone weekly cron script. For each non-global region: reads the latest snapshot + recent regime transitions from #2981's history log, calls the LLM once per region, writes to `intelligence:regional-briefs:v1:weekly:{region}` with 8-day TTL (survives one missed weekly run).
New module: `scripts/regional-snapshot/weekly-brief.mjs`
`generateWeeklyBrief(region, snapshot, transitions, opts?) -> brief`
Prompt includes: current regime, 7-axis balance, active triggers, last 7 days of regime transitions, latest narrative situation + 7d outlook. Output: `{ situation_recap, regime_trajectory, key_developments[], risk_outlook }`. Same provider chain + validate-in-loop + injectable-callLlm pattern as narrative.mjs.
Proto + RPC: `GetRegionalBrief`
`GetRegionalBriefRequest { region_id } -> GetRegionalBriefResponse { brief: RegionalBrief }`
`RegionalBrief { region_id, generated_at, period_start, period_end, situation_recap, regime_trajectory, key_developments[], risk_outlook, provider, model }`
Handler + premium gating
`get-regional-brief.ts` — simple `getCachedJson` read + `adaptBrief` adapter. Returns `upstreamUnavailable: true` on Redis failure (matching #2981 pattern). Premium-gated via `PREMIUM_RPC_PATHS` + `RPC_CACHE_TIER`.
Testing
27 new unit tests. `npm run test:data`: 4651/4651 pass. Typecheck + lint clean.
Post-Deploy Monitoring & Validation
PR sequence