666ghj/MiroFish · error · RuntimeError

Zep updater for {simulation_id} is still active

Error message

Zep updater for {simulation_id} is still active

What it means

Raised by ZepGraphMemoryUpdater.discard_inactive_updater when graph destruction tries to remove an updater from the class-level registry but the updater still has _running set or its worker thread is alive. The guard exists so explicit graph deletion never discards an updater that is still writing to the graph being destroyed.

Source

Thrown at backend/app/services/zep_graph_memory_updater.py:717

    def get_simulation_ids(cls) -> List[str]:
        """Return every simulation with a retained updater."""

        with cls._lock:
            return sorted(cls._updaters)

    @classmethod
    def discard_inactive_updater(cls, simulation_id: str) -> bool:
        """Discard a failed, fully stopped updater during graph destruction."""

        with cls._lock:
            updater = cls._updaters.get(simulation_id)
            if updater is None:
                return False
            worker_alive = bool(
                updater._worker_thread and updater._worker_thread.is_alive()
            )
            if updater._running or worker_alive:
                raise RuntimeError(
                    f"Zep updater for {simulation_id} is still active"
                )
            cls._updaters.pop(simulation_id, None)
        logger.warning(
            "Discarded incomplete Zep updater during explicit graph deletion: "
            "simulation_id=%s, graph_id=%s",
            simulation_id,
            updater.graph_id,
        )
        return True
    
    @classmethod
    def stop_updater(cls, simulation_id: str):
        """停止并移除模拟的更新器"""
        with cls._lock:
            updater = cls._updaters.get(simulation_id)
        if updater is None:
            return

View on GitHub (pinned to b5b53acc57)

Solutions

  1. Call stop() (with a drain deadline) and wait for the worker thread to join before discard_inactive_updater
  2. If stop() previously failed, inspect updater state (get_stats / logs) and retry stop with a longer budget
  3. Only after the updater is fully quiescent call discard_inactive_updater again
  4. Never force-pop the registry entry while the thread is alive — the thread would keep writing to a deleted graph

Example fix

# before
updater.stop()
ZepGraphMemoryUpdater.discard_inactive_updater(sim_id)  # thread may still be alive

# after
updater.stop()
updater._worker_thread.join(timeout=30)
if updater._worker_thread.is_alive():
    raise RuntimeError("worker thread did not exit; refusing to delete graph")
ZepGraphMemoryUpdater.discard_inactive_updater(sim_id)
Defensive patterns

Strategy: validation

Validate before calling

updater = ZepGraphMemoryUpdater._updaters.get(sim_id)
if updater and (updater._running or (updater._worker_thread and updater._worker_thread.is_alive())):
    raise RuntimeError(f"cannot delete graph: updater for {sim_id} still active")
ZepGraphMemoryUpdater.discard_inactive_updater(sim_id)

Try / catch

try:
    ZepGraphMemoryUpdater.discard_inactive_updater(sim_id)
except RuntimeError:
    # stop properly first, then retry discard
    updater.stop(); updater._worker_thread.join(timeout=30)
    ZepGraphMemoryUpdater.discard_inactive_updater(sim_id)

Prevention

When it happens

Trigger: Calling discard_inactive_updater(simulation_id) after stop() failed or before the worker thread fully exited (stop initiated but thread.join not completed, or stop raised and left _running true). The class lock is held while raising, so the updater stays registered.

Common situations: Error-path cleanup: a previous stop() raised (e.g. Zep API error during final flush) leaving the updater half-stopped, then the user deletes the graph; or a race where deletion is requested moments after stop while the worker is mid-batch.

Related errors


AI-assisted analysis of 666ghj/MiroFish@b5b53acc57 (2026-08-14). Data as JSON: /api/errors/2b7e7243adb4ce7a. Report an issue: GitHub.