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

> Discover how MiroFish persists and retrieves simulation run states using JSON files and cached data for effortless resuming and detailed analysis.

- Repository: [BaiFu/mirofish](https://github.com/666ghj/mirofish)
- Tags: internals
- Published: 2026-02-23

---

**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`](https://github.com/666ghj/mirofish/blob/main/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`](https://github.com/666ghj/mirofish/blob/main/backend/app/services/simulation_runner.py). It encapsulates all runtime metadata required to reconstruct the simulation’s progress:

```python
@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`](https://github.com/666ghj/mirofish/blob/main/backend/app/services/simulation_runner.py) defines the base path:

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

```

The persisted state is stored as [`run_state.json`](https://github.com/666ghj/mirofish/blob/main/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`](https://github.com/666ghj/mirofish/blob/main/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:

```python
@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`](https://github.com/666ghj/mirofish/blob/main/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`](https://github.com/666ghj/mirofish/blob/main/run_state.json). If the file is missing, it returns `None`, signaling that the simulation has not started or the state was purged:

```python
@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:

```python
@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:

```python
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`](https://github.com/666ghj/mirofish/blob/main/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`](https://github.com/666ghj/mirofish/blob/main/backend/app/api/simulation.py) surfaces the persisted state to clients:

```python
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`](https://github.com/666ghj/mirofish/blob/main/run_state.json)) stored in `uploads/simulations/<simulation_id>/`.
- The **`SimulationRunState`** dataclass in [`backend/app/services/simulation_runner.py`](https://github.com/666ghj/mirofish/blob/main/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`](https://github.com/666ghj/mirofish/blob/main/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`](https://github.com/666ghj/mirofish/blob/main/run_state.json). This path is defined by the `RUN_STATE_DIR` constant in [`backend/app/services/simulation_runner.py`](https://github.com/666ghj/mirofish/blob/main/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`](https://github.com/666ghj/mirofish/blob/main/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.