Compare commits

...
Author SHA1 Message Date
praisonai-triage-agent[bot]andMervin Praison d9f2eef104 fix: isolate _approvals_lock on agent deepcopy
asyncio.Lock deepcopy silently shares internal waiter state, so cloned
agents shared approval-lock internals with the original. Replace it with
a fresh asyncio.Lock in Agent.__deepcopy__, consistent with the existing
cache/cost lock handling, and add a regression test.

Co-authored-by: Mervin Praison <[email protected]>
2026-08-14 12:21:47 +00:00
2 changed files with 14 additions and 1 deletions

No files matched your search

@@ -2457,7 +2457,8 @@ Your Goal: {self.goal}
"""Custom deepcopy that creates fresh threading primitives.
threading primitives cannot be deep-copied on all supported Python
versions. This hook replaces the Agent-owned locks directly, while
versions, and ``asyncio.Lock`` copies silently share internal waiter
state. This hook replaces the Agent-owned locks directly, while
lock-bearing state wrappers provide their own deepcopy hooks, so the
clone receives independent state and fresh locks.
"""
@@ -2470,6 +2471,8 @@ Your Goal: {self.goal}
object.__setattr__(result, k, threading.RLock())
elif k == "_cost_lock":
object.__setattr__(result, k, threading.Lock())
elif k == "_approvals_lock":
object.__setattr__(result, k, asyncio.Lock())
else:
object.__setattr__(result, k, copy.deepcopy(v, memo))
return result
@@ -94,3 +94,13 @@ class TestAgentDeepCopy:
assert cloned._Agent__chat_history_state is not agent._Agent__chat_history_state
assert cloned._Agent__snapshot_state is not agent._Agent__snapshot_state
assert cloned._turn_tools_lock is not agent._turn_tools_lock
def test_deepcopy_isolates_approvals_lock(self):
import asyncio
agent = self._make_agent()
cloned = copy.deepcopy(agent)
assert cloned._approvals_lock is not agent._approvals_lock
assert isinstance(cloned._approvals_lock, asyncio.Lock)
assert cloned._pending_approvals is not agent._pending_approvals