Skip to content

feat(simulation): MiroFish Phase 2 — theater-limited simulation runner#2220

Merged
koala73 merged 5 commits into
mainfrom
feat/mirofish-phase2-simulation-runner
Mar 25, 2026
Merged

feat(simulation): MiroFish Phase 2 — theater-limited simulation runner#2220
koala73 merged 5 commits into
mainfrom
feat/mirofish-phase2-simulation-runner

Conversation

@koala73

@koala73 koala73 commented Mar 24, 2026

Copy link
Copy Markdown
Owner

Why this PR?

Phase 1 (PR #2204) built buildSimulationPackageFromDeepSnapshot — it writes simulation-package.json to R2 with selected theaters, entities, event seeds, constraints, and evaluation targets. Phase 2 prerequisites (PR #2219) added getSimulationPackage RPC so agents can discover the latest package pointer from Redis.

This PR closes the loop: a simulation runner reads the package, runs a 2-round LLM actor simulation for maritime chokepoint + energy/logistics theaters, and writes simulation-outcome.json back to R2 with a Redis pointer. A new getSimulationOutcome RPC exposes the pointer to clients.

What changed

scripts/seed-forecasts.mjs — simulation runner core

  • Constants: SIMULATION_OUTCOME_LATEST_KEY, task queue keys, lock keys, round token budgets, TTLs
  • isMaritimeChokeEnergyCandidate(theater): Phase 2 scope gate — passes only maritime chokepoint + energy/logistics theaters
  • buildSimulationRound1SystemPrompt(theater, pkg): structured prompt from entities, event seeds, constraints, evaluation targets, simulationRequirement. Produces 3 divergent paths (escalation/containment/spillover) with initialReactions and dominantReactions. All interpolated theater fields sanitized against prompt injection
  • buildSimulationRound2SystemPrompt(theater, pkg, round1): builds 72h evolution prompt from round 1 path summaries. Produces keyActors, roundByRoundEvolution (2 entries), timingMarkers, stabilizers, invalidators, confidence, globalObservations, confidenceNotes
  • extractSimulationRoundPayload(text, round): 3-stage extractor (strip think tags → fenced blocks → raw JSON → extractFirstJsonObject fallback) following same pattern as extractImpactExpansionPayload
  • runTheaterSimulation(theater, pkg): calls callForecastLLM for round 1, then conditionally round 2 if round 1 succeeded. Round 2 failure is partial-valid (round 1 results preserved)
  • writeSimulationOutcome(pkg, outcome, { storageConfig }): writes simulation-outcome.json to R2 via buildSimulationOutcomeKey, then writes SIMULATION_OUTCOME_LATEST_KEY to Redis with { runId, outcomeKey, schemaVersion, theaterCount, generatedAt } and TRACE_REDIS_TTL_SECONDS TTL
  • Task queue: enqueueSimulationTask, claimSimulationTask (NX lock deduplication), completeSimulationTask, processNextSimulationTask (idempotency check against Redis pointer before LLM calls), runSimulationWorker({ once, runId })

scripts/process-simulation-tasks.mjs — standalone entry point

12-line script: loads env, delegates to runSimulationWorker. Supports --once and --run-id=<id> flags.

Proto + RPC — getSimulationOutcome

  • New proto/worldmonitor/forecast/v1/get_simulation_outcome.proto with GetSimulationOutcomeRequest (optional runId query param) and GetSimulationOutcomeResponse (found, runId, outcomeKey, schemaVersion, theaterCount, generatedAt, note, error)
  • service.proto updated with import + RPC
  • make generate regenerated TypeScript server + client types
  • server/worldmonitor/forecast/v1/get-simulation-outcome.ts: identical getRawJson + markNoCacheResponse pattern from PR feat(forecast): Phase 2 simulation package read path #2219
  • server/worldmonitor/forecast/v1/handler.ts: wired getSimulationOutcome
  • server/gateway.ts: slow cache tier (same reasoning as simulation package)

Bootstrap/health registration

  • api/health.js: simulationOutcomeLatest added to STANDALONE_KEYS + ON_DEMAND_KEYS (WARN if empty, not CRIT — only written after simulation runs, not seeded)
  • cache-keys.ts and bootstrap.js not touched (on-demand key, not seeded)

Tests — tests/forecast-trace-export.test.mjs

14 new tests across 4 suites:

  • Prompt builders: theater label, 3 path IDs, entity IDs, seed IDs, simulationRequirement in R1; R1 summaries, ROUND 2 marker, actor IDs in R2
  • extractSimulationRoundPayload: valid R1/R2, fenced blocks, think tag stripping, invalid JSON, missing paths, no valid pathId, extractFirstJsonObject fallback
  • Outcome key builder: key ends in simulation-outcome.json, canonical Redis key, schema version v1
  • writeSimulationOutcome: null when storageConfig absent, null when pkg has no runId

All 2316 tests pass. TypeScript clean.

Scope boundary (Phase 2)

Only maritime chokepoint + energy/logistics theaters are simulated. Third-order commodity expansion and additional theater classes are Phase 3.

Post-Deploy Monitoring & Validation

  • Trigger a run: node scripts/seed-forecasts.mjs && node scripts/process-simulation-tasks.mjs --once
  • Logs to watch: [processNextSimulationTask], [runSimulationWorker], [writeSimulationOutcome]
  • Expected healthy: status: completed, theaterCount >= 1, R2 key forecast:simulation-outcome:latest present in Redis
  • RPC check: GET /api/forecast/v1/get-simulation-outcome returns { found: true, theaterCount: N }
  • Failure signal: status: completed_no_theaters → package has no maritime chokepoint + energy/logistics theaters; status: failed → LLM or R2 write error — check logs
  • Validation window: first deep run after deploy
  • Owner: @koala73

Compound Engineering v2.49.0
🤖 Generated with Claude Sonnet 4.6 (200K context) via Claude Code

@vercel

vercel Bot commented Mar 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment Mar 25, 2026 9:39am

Request Review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@mintlify

mintlify Bot commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
WorldMonitor 🟢 Ready View Preview Mar 24, 2026, 7:25 PM

koala73 added a commit that referenced this pull request Mar 25, 2026
Security (P1 #18):
- sanitizeForPrompt() applied to all entity/seed fields interpolated into
  Round 1 prompt (entityId, class, stance, seedId, type, timing)
- sanitizeForPrompt() applied to actorId and entityIds in Round 2 prompt
- sanitizeForPrompt() + length caps applied to all LLM array fields written
  to R2 (dominantReactions, stabilizers, invalidators, keyActors, timingMarkers)

Validation (P1 #19):
- Added validateRunId() regex guard
- Applied in enqueueSimulationTask() and processNextSimulationTask() loop

Type safety (P1 #20):
- Added isOutcomePointer() and isPackagePointer() type guards in TS handlers
- Replaced unsafe as-casts with runtime-validated guards in both handlers

Correctness (P2 #22):
- Log warning when pkgPointer.runId does not match task runId

Architecture (P2 #24):
- isMaritimeChokeEnergyCandidate() accepts both flat and nested topBucketId
- Call site simplified to pass theater directly

Performance (P2 #25):
- SIMULATION_ROUND1_MAX_TOKENS raised 1800 to 2200
- Added max 3 initialReactions instruction to Round 1 prompt

Maintainability (P2 #26):
- Simulation pointer keys exported from server/_shared/cache-keys.ts
- Both TS handlers import from shared location

Documentation (P2 #27):
- Strengthened runId no-op description in proto and OpenAPI spec
koala73 added 5 commits March 25, 2026 13:30
Adds the simulation execution layer that consumes simulation-package.json
and produces simulation-outcome.json for maritime chokepoint + energy/logistics
theaters, closing the WorldMonitor → MiroFish handoff loop.

Changes:
- scripts/seed-forecasts.mjs: 2-round LLM simulation runner (prompt builders,
  JSON extractor, runTheaterSimulation, writeSimulationOutcome, task queue
  with NX dedup lock, runSimulationWorker poll loop)
- scripts/process-simulation-tasks.mjs: standalone worker entry point
- proto: GetSimulationOutcome RPC + make generate
- server/worldmonitor/forecast/v1/get-simulation-outcome.ts: RPC handler
- server/gateway.ts: slow tier for get-simulation-outcome
- api/health.js: simulationOutcomeLatest in STANDALONE + ON_DEMAND keys
- tests: 14 new tests for simulation runner functions
Security (P1 #18):
- sanitizeForPrompt() applied to all entity/seed fields interpolated into
  Round 1 prompt (entityId, class, stance, seedId, type, timing)
- sanitizeForPrompt() applied to actorId and entityIds in Round 2 prompt
- sanitizeForPrompt() + length caps applied to all LLM array fields written
  to R2 (dominantReactions, stabilizers, invalidators, keyActors, timingMarkers)

Validation (P1 #19):
- Added validateRunId() regex guard
- Applied in enqueueSimulationTask() and processNextSimulationTask() loop

Type safety (P1 #20):
- Added isOutcomePointer() and isPackagePointer() type guards in TS handlers
- Replaced unsafe as-casts with runtime-validated guards in both handlers

Correctness (P2 #22):
- Log warning when pkgPointer.runId does not match task runId

Architecture (P2 #24):
- isMaritimeChokeEnergyCandidate() accepts both flat and nested topBucketId
- Call site simplified to pass theater directly

Performance (P2 #25):
- SIMULATION_ROUND1_MAX_TOKENS raised 1800 to 2200
- Added max 3 initialReactions instruction to Round 1 prompt

Maintainability (P2 #26):
- Simulation pointer keys exported from server/_shared/cache-keys.ts
- Both TS handlers import from shared location

Documentation (P2 #27):
- Strengthened runId no-op description in proto and OpenAPI spec
…andler coverage

Two tests identified as missing during PR #2220 review:

1. isMaritimeChokeEnergyCandidate flat-shape tests — covers the || candidate.topBucketId
   normalization added in the P1/P2 review pass. The existing tests only used the nested
   marketContext.topBucketId shape; this adds the flat root-field shape that arrives from
   the simulation-package.json JSON (selectedTheaters entries have topBucketId at root).

2. getSimulationOutcome handler structural tests — verifies the isOutcomePointer guard,
   found:false NOT_FOUND return, found:true success path, note population on runId mismatch,
   and redis_unavailable error string. Follows the readSrc static-analysis pattern used
   elsewhere in server-handlers.test.mjs (handler imports Redis so full integration test
   would require a test Redis instance).
@koala73
koala73 force-pushed the feat/mirofish-phase2-simulation-runner branch from a7e066f to 95b81e3 Compare March 25, 2026 09:37
@koala73
koala73 merged commit 01f6057 into main Mar 25, 2026
9 checks passed
@koala73
koala73 deleted the feat/mirofish-phase2-simulation-runner branch March 25, 2026 09:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant