Compare commits

...

1 Commits

Author SHA1 Message Date
kimi
57f4f37a9b fix: make _get_loop_agent singleton thread-safe with double-checked locking
Uses threading.Lock with double-checked locking pattern to prevent
race conditions when multiple agentic loops start concurrently.

Fixes #446

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 14:18:25 -04:00
2 changed files with 51 additions and 3 deletions

View File

@@ -18,6 +18,7 @@ from __future__ import annotations
import asyncio import asyncio
import logging import logging
import re import re
import threading
import time import time
import uuid import uuid
from collections.abc import Callable from collections.abc import Callable
@@ -59,6 +60,7 @@ class AgenticResult:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
_loop_agent = None _loop_agent = None
_loop_agent_lock = threading.Lock()
def _get_loop_agent(): def _get_loop_agent():
@@ -66,12 +68,18 @@ def _get_loop_agent():
Returns the same type of agent as `create_timmy()` but with a Returns the same type of agent as `create_timmy()` but with a
dedicated session so it doesn't pollute the main chat history. dedicated session so it doesn't pollute the main chat history.
Thread-safe: uses a lock to prevent duplicate agent creation
when multiple loops start concurrently.
""" """
global _loop_agent global _loop_agent
if _loop_agent is None: if _loop_agent is not None:
from timmy.agent import create_timmy return _loop_agent
with _loop_agent_lock:
if _loop_agent is None:
from timmy.agent import create_timmy
_loop_agent = create_timmy() _loop_agent = create_timmy()
return _loop_agent return _loop_agent

View File

@@ -104,6 +104,46 @@ class TestGetLoopAgent:
finally: finally:
al._loop_agent = saved al._loop_agent = saved
def test_thread_safe_creation(self):
"""Concurrent calls must only create one agent (thread-safety)."""
import threading
import timmy.agentic_loop as al
saved = al._loop_agent
try:
al._loop_agent = None
mock_agent = MagicMock()
call_count = 0
barrier = threading.Barrier(4)
original_create = MagicMock(return_value=mock_agent)
def slow_create():
nonlocal call_count
call_count += 1
return original_create()
results = [None] * 4
def worker(idx):
barrier.wait()
results[idx] = al._get_loop_agent()
with patch("timmy.agent.create_timmy", side_effect=slow_create):
threads = [threading.Thread(target=worker, args=(i,)) for i in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
# All threads got the same agent
assert all(r is mock_agent for r in results)
# create_timmy called exactly once
assert call_count == 1
finally:
al._loop_agent = saved
# ── _broadcast_progress ────────────────────────────────────────────────────── # ── _broadcast_progress ──────────────────────────────────────────────────────