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 runView on GitHub (pinned to 3e82f13042)
Solutions
- 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
- Catch RunBusyError and wait for the active run to finish (await its completion event) then decide whether a re-run is still needed
- Debounce/schedule consolidation so overlapping triggers for the same layer/key collapse into one run
- 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
- Always check active_for(layer, key) before start
- Debounce scheduled consolidation per (layer, key)
- Expose run status in UIs so users see a run is already going
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
- {exc}
- preferences.md is not auto-consolidated
- unknown layer {layer!r}
- preferences.md is not auto-consolidated
- cancel the active run before undoing memory edits
AI-assisted analysis of HKUDS/DeepTutor@3e82f13042 (2026-08-27).
Data as JSON: /api/errors/3a03aa273a111b5d.
Report an issue: GitHub.