Summary
approve_or_deny_turn (service.py:177-317) performs sequence allocation, result persistence, compaction, companion history refresh, and post-turn hooks without acquiring the per-thread asyncio.Lock that _execute_agent_turn uses.
Failure Scenario
- User sends a chat message to thread X → acquires
get_thread_lock(X)
- Simultaneously, user approves a pending tool call on the same thread X →
approve_or_deny_turn runs with no lock
- Both paths call
reserve_message_sequences, persist_agent_result, compact_thread_context, and _refresh_companion_history concurrently
- Result: sequence ID collisions, corrupted companion history cache, duplicated or interleaved messages
Fix
Wrap the body of approve_or_deny_turn in async with get_thread_lock(run.thread_id): after resolving the checkpoint and determining the thread ID.
Files
apps/server/src/anima_server/services/agent/service.py:177-317
Severity
High — data corruption under concurrent approval + chat on same thread.
Summary
approve_or_deny_turn(service.py:177-317) performs sequence allocation, result persistence, compaction, companion history refresh, and post-turn hooks without acquiring the per-threadasyncio.Lockthat_execute_agent_turnuses.Failure Scenario
get_thread_lock(X)approve_or_deny_turnruns with no lockreserve_message_sequences,persist_agent_result,compact_thread_context, and_refresh_companion_historyconcurrentlyFix
Wrap the body of
approve_or_deny_turninasync with get_thread_lock(run.thread_id):after resolving the checkpoint and determining the thread ID.Files
apps/server/src/anima_server/services/agent/service.py:177-317Severity
High — data corruption under concurrent approval + chat on same thread.