HKUDS/DeepTutor · warning · RunBusyError

a run is already in progress for {layer}/{key}

Error message

a run is already in progress for {layer}/{key}

What it means

RunManager.start raises RunBusyError when a consolidation run is already active for the same layer/key pair. The manager serializes runs per (layer, key) under an async lock so two consolidators never write the same memory document concurrently; starting a second run for the same target while the first is still executing is rejected.

Source

Thrown at deeptutor/services/memory/consolidator/runs.py:175

        self,
        *,
        layer: str,
        key: str,
        mode: RunMode,
        runner: Callable[[Callable[[dict[str, Any]], Awaitable[None]]], Awaitable[None]],
        params: dict[str, Any] | None = None,
        language: str = "en",
        user_label: str = "anonymous",
    ) -> Run:
        """Register and kick off a new run.

        ``runner`` is an awaitable factory: takes a ``on_event`` callback
        and runs the consolidator mode. The manager wires the callback to
        the event buffer + waiter machinery.
        """
        async with self._lock:
            if self.active_for(layer, key) is not None:
                raise RunBusyError(f"a run is already in progress for {layer}/{key}")
            run = Run(
                id=uuid.uuid4().hex,
                layer=layer,
                key=key,
                mode=mode,
                params=dict(params or {}),
                language=language,
                user_label=user_label,
                status="queued",
                started_at=_now_iso(),
            )
            self._runs[run.id] = run
            self._order.append(run.id)
            self._active[(layer, key)] = run.id
            self._evict_if_needed()

        run._task = asyncio.create_task(self._drive(run, runner))
        return run

View on GitHub (pinned to 3e82f13042)

Solutions

  1. Check manager.active_for(layer, key) before starting; if a run exists, subscribe to its events or poll its status instead of starting a new one
  2. Catch RunBusyError and wait for the active run to finish (await its completion event) then decide whether a re-run is still needed
  3. Debounce/schedule consolidation so overlapping triggers for the same layer/key collapse into one run
  4. Cancel the active run first via the cancel API if the new run must supersede it

Example fix

// before
run = await manager.start(layer, key, mode, runner)

// after
if manager.active_for(layer, key) is not None:
    await manager.wait_for_completion(layer, key)
run = await manager.start(layer, key, mode, runner)
Defensive patterns

Strategy: fallback

Validate before calling

if manager.active_for(layer, key) is not None:
    existing = manager.active_for(layer, key)
    # subscribe/poll existing instead of starting
    return await manager.wait_for_completion(layer, key)

Try / catch

try:
    run = await manager.start(layer, key, mode, runner)
except RunBusyError:
    await manager.wait_for_completion(layer, key)  # piggyback on active run
    run = None  # decide whether a fresh run is still needed

Prevention

When it happens

Trigger: Calling runs.start(layer, key, ...) twice without awaiting/finishing the first run — e.g. a scheduler firing consolidation again while a previous run is still in progress, or refs_in_span_l3 triggering a nested/overlapping run for the same key.

Common situations: Cron/background consolidation overlapping with a user-triggered consolidation; concurrent WebSocket/API sessions consolidating the same KB; retry logic that starts a new run instead of polling the active one.

Related errors


AI-assisted analysis of HKUDS/DeepTutor@3e82f13042 (2026-08-27). Data as JSON: /api/errors/3a03aa273a111b5d. Report an issue: GitHub.