Compare commits

...

1 Commits

Author SHA1 Message Date
kimi
57f4f37a9b fix: make _get_loop_agent singleton thread-safe with double-checked locking
Some checks failed
Tests / lint (pull_request) Successful in 3s
Tests / test (pull_request) Failing after 59s
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 logging
import re
import threading
import time
import uuid
from collections.abc import Callable
@@ -59,6 +60,7 @@ class AgenticResult:
# ---------------------------------------------------------------------------
_loop_agent = None
_loop_agent_lock = threading.Lock()
def _get_loop_agent():
@@ -66,12 +68,18 @@ def _get_loop_agent():
Returns the same type of agent as `create_timmy()` but with a
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
if _loop_agent is None:
from timmy.agent import create_timmy
if _loop_agent is not None:
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

View File

@@ -104,6 +104,46 @@ class TestGetLoopAgent:
finally:
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 ──────────────────────────────────────────────────────