fix(sdk): use contextvars for per-call hooks to prevent concurrent run() race
Nanobot.run() previously mutated the shared self._loop._extra_hooks attribute under a try/finally. When two run() calls with different session_keys execute concurrently, they overwrite each other's hook lists — the second call saves the first call's hooks as 'prev', and the finally block restores stale state. Use a contextvars.ContextVar instead, which is per-task in asyncio. This gives each concurrent run() call its own hook list without changing any function signatures. Falls back to self._extra_hooks when no per-call hooks are set (non-SDK usage).
This commit is contained in:
+11
-2
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextvars
|
||||
import dataclasses
|
||||
import os
|
||||
import time
|
||||
@@ -75,6 +76,12 @@ if TYPE_CHECKING:
|
||||
)
|
||||
from nanobot.cron.service import CronService
|
||||
|
||||
# Per-call hooks for SDK Nanobot.run() — avoids mutating shared _extra_hooks
|
||||
# when multiple run() calls execute concurrently on the same AgentLoop.
|
||||
_per_call_hooks: contextvars.ContextVar[list[AgentHook] | None] = contextvars.ContextVar(
|
||||
"_per_call_hooks", default=None,
|
||||
)
|
||||
|
||||
|
||||
class TurnState(Enum):
|
||||
RESTORE = auto()
|
||||
@@ -720,8 +727,10 @@ class AgentLoop:
|
||||
on_iteration=lambda iteration: setattr(self, "_current_iteration", iteration),
|
||||
)
|
||||
hook: AgentHook = loop_hook
|
||||
if not ephemeral and self._extra_hooks:
|
||||
hook = CompositeHook([loop_hook] + self._extra_hooks)
|
||||
per_call = _per_call_hooks.get()
|
||||
extra_hooks = per_call if per_call is not None else (self._extra_hooks if not ephemeral else None)
|
||||
if extra_hooks:
|
||||
hook = CompositeHook([loop_hook] + extra_hooks)
|
||||
|
||||
async def _checkpoint(payload: dict[str, Any]) -> None:
|
||||
if session is None:
|
||||
|
||||
Reference in New Issue
Block a user