How Simulation Run States Are Persisted and Retrieved in MiroFish for Resuming and Analysis

MiroFish persists simulation run states to a JSON file on every state change and retrieves them via an in-memory cache with disk fallback, enabling crash recovery and real-time monitoring.

MiroFish runs long‑running OASIS simulations in the background that must survive process restarts and server crashes. The simulation run states persisted and retrieved by the system enable real‑time progress tracking, post‑run analysis, and seamless resumption of interrupted workflows. This persistence logic is implemented in the 666ghj/mirofish repository, specifically within the backend/app/services/simulation_runner.py module.

Architecture of Simulation State Persistence

The SimulationRunState Dataclass

The core data structure is the SimulationRunState dataclass defined in backend/app/services/simulation_runner.py. It encapsulates all runtime metadata required to reconstruct the simulation’s progress:

@dataclass
class SimulationRunState:
    """模拟运行状态(实时)"""
    simulation_id: str
    runner_status: RunnerStatus = RunnerStatus.IDLE
    current_round: int = 0
    total_rounds: int = 0
    simulated_hours: int = 0
    total_simulation_hours: int = 0
    # … platform‑specific counters, recent actions, timestamps, error info …

All fields are JSON‑serializable, allowing the state to be written to disk and restored without data loss.

JSON File Storage Location

Each simulation receives a dedicated directory under uploads/simulations/<simulation_id>/. The constant RUN_STATE_DIR in backend/app/services/simulation_runner.py defines the base path:

RUN_STATE_DIR = os.path.join(
    os.path.dirname(__file__),
    '../../uploads/simulations'
)

The persisted state is stored as run_state.json inside the simulation‑specific subdirectory, ensuring isolation between concurrent runs.

Persisting Simulation States

The _save_run_state Method

The _save_run_state classmethod in backend/app/services/simulation_runner.py handles atomic writes to disk. It converts the dataclass to a dictionary via to_detail_dict() and writes formatted JSON:

@classmethod
def _save_run_state(cls, state: SimulationRunState):
    sim_dir = os.path.join(cls.RUN_STATE_DIR, state.simulation_id)
    os.makedirs(sim_dir, exist_ok=True)
    state_file = os.path.join(sim_dir, "run_state.json")
    data = state.to_detail_dict()
    with open(state_file, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
    cls._run_states[state.simulation_id] = state

After writing to disk, the method updates the in‑memory cache (cls._run_states) to ensure subsequent reads are served from RAM.

When States Are Saved

The monitor thread _monitor_simulation triggers persistence after every significant event. Located in backend/app/services/simulation_runner.py, this thread parses per‑platform actions.jsonl logs, updates counters, appends recent actions, and calls cls._save_run_state(state) every few seconds. When the subprocess terminates, the thread sets runner_status to COMPLETED or FAILED, records timestamps or error messages, and performs a final save.

Retrieving Simulation States

The _load_run_state Method

The _load_run_state classmethod reconstructs a SimulationRunState object from run_state.json. If the file is missing, it returns None, signaling that the simulation has not started or the state was purged:

@classmethod
def _load_run_state(cls, simulation_id: str) -> Optional[SimulationRunState]:
    state_file = os.path.join(cls.RUN_STATE_DIR, simulation_id, "run_state.json")
    if not os.path.exists(state_file):
        return None
    with open(state_file, 'r', encoding='utf-8') as f:
        data = json.load(f)
    state = SimulationRunState(
        simulation_id=simulation_id,
        runner_status=RunnerStatus(data.get("runner_status", "idle")),
        current_round=data.get("current_round", 0),
        total_rounds=data.get("total_rounds", 0),
        # … populate the rest of the fields …

    )
    # Re‑hydrate recent actions

    for a in data.get("recent_actions", []):
        state.recent_actions.append(AgentAction(**a))
    return state

The get_run_state Public Accessor

The get_run_state method provides a unified interface for the API layer. It first checks the in‑memory cache (cls._run_states), then falls back to disk via _load_run_state, and finally repopulates the cache:

@classmethod
def get_run_state(cls, simulation_id: str) -> Optional[SimulationRunState]:
    if simulation_id in cls._run_states:
        return cls._run_states[simulation_id]
    state = cls._load_run_state(simulation_id)
    if state:
        cls._run_states[simulation_id] = state
    return state

This two‑tier strategy ensures low‑latency reads for active simulations while guaranteeing durability across process restarts.

Practical Usage Examples

Querying Current State

To inspect a running or completed simulation, call SimulationRunner.get_run_state with the simulation ID:

from backend.app.services.simulation_runner import SimulationRunner

state = SimulationRunner.get_run_state("sim_2026_02_23_001")
if state:
    print(f"Round {state.current_round}/{state.total_rounds}")
    print(f"Twitter actions: {state.twitter_actions_count}")
    print(f"Runner status: {state.runner_status.value}")
else:
    print("Simulation not found or not started yet.")

This pattern is used by the front‑end to render progress bars and status badges.

Resuming After a Crash

When the backend process restarts, the in‑memory cache is empty, but the JSON files remain intact. The first call to SimulationRunner.get_run_state automatically loads the persisted state from run_state.json, repopulates the cache, and returns the full SimulationRunState object. The stored process_pid field can be inspected to determine whether the original OS process is still alive; if not, the system can launch a fresh subprocess and continue from the last recorded round.

API Integration

The HTTP endpoint GET /simulation/<simulation_id>/ in backend/app/api/simulation.py surfaces the persisted state to clients:

run_state = SimulationRunner.get_run_state(sim.simulation_id)
if run_state:
    sim_dict["current_round"] = run_state.current_round
    sim_dict["runner_status"] = run_state.runner_status.value
    sim_dict["total_rounds"] = (
        run_state.total_rounds
        if run_state.total_rounds > 0
        else recommended_rounds
    )

This integration allows the React front‑end to display real‑time updates without polling the subprocess directly.

Summary

  • MiroFish persists simulation run states to a JSON file (run_state.json) stored in uploads/simulations/<simulation_id>/.
  • The SimulationRunState dataclass in backend/app/services/simulation_runner.py defines the schema, including round counters, platform‑specific metrics, and recent actions.
  • _save_run_state writes atomic snapshots after every significant event, while _load_run_state reconstructs the object from disk.
  • get_run_state provides a two‑tier cache (memory → disk) for low‑latency reads and crash resilience.
  • The API layer in backend/app/api/simulation.py exposes these states via HTTP, enabling real‑time front‑end monitoring and post‑run analysis.

Frequently Asked Questions

Where does MiroFish store simulation run states?

MiroFish stores each simulation’s state in a dedicated directory under uploads/simulations/<simulation_id>/ as a file named run_state.json. This path is defined by the RUN_STATE_DIR constant in backend/app/services/simulation_runner.py.

How does MiroFish handle state retrieval after a server restart?

After a restart, the in‑memory cache is empty. The first call to SimulationRunner.get_run_state checks the cache, misses, and falls back to _load_run_state, which reads run_state.json from disk, reconstructs the SimulationRunState object, and repopulates the cache.

What information is included in the persisted simulation state?

The SimulationRunState dataclass includes the simulation ID, current and total rounds, simulated hours, runner status (idle/running/completed/failed), platform‑specific action counts, recent agent actions, timestamps, error messages, and the OS process ID.

How often does MiroFish save the simulation run state?

The state is persisted after every significant mutation. Specifically, the _monitor_simulation thread calls _save_run_state every few seconds while parsing actions.jsonl logs, and a final save occurs when the subprocess terminates with a status of completed or failed.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →