From 982808c435e0bd4a2403b5c95c487e3733ceb7ee Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Fri, 17 Apr 2026 18:49:08 +0700 Subject: [PATCH] fix(backend): skip append_message_if when session lock not acquired MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Redis is unavailable, _get_session_lock yields False — without this guard two concurrent callers could both read the same DB state, both pass the predicate, and both append, producing duplicate messages. append_message_if only powers non-critical fire-and-forget flows (decompose_goal auto-approve), so skipping is safer than risking a duplicate; the user can still trigger the action manually. --- .../backend/backend/copilot/model.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/autogpt_platform/backend/backend/copilot/model.py b/autogpt_platform/backend/backend/copilot/model.py index 9b82682bca..a9379cfb5b 100644 --- a/autogpt_platform/backend/backend/copilot/model.py +++ b/autogpt_platform/backend/backend/copilot/model.py @@ -726,9 +726,22 @@ async def append_message_if( the append are one atomic operation — no race with concurrent appends. Returns the updated session on append, or ``None`` if the predicate - rejected, the session no longer exists, or the append failed. + rejected, the session no longer exists, the lock could not be acquired, + or the append failed. """ - async with _get_session_lock(session_id) as _lock_acquired: + async with _get_session_lock(session_id) as lock_acquired: + # Without the lock, concurrent callers could both read the same DB + # state, both pass the predicate, and both append — producing + # duplicate messages. Since ``append_message_if`` only powers + # non-critical fire-and-forget flows (e.g. decompose_goal + # auto-approve), skipping on lock failure is safer than risking a + # duplicate. The user can still trigger the action manually. + if not lock_acquired: + logger.warning( + "append_message_if: skipping for session %s (lock not acquired)", + session_id, + ) + return None # Read from DB directly — the Redis cache can be stale because the # executor's upsert_chat_session overwrites it with in-memory copies # during streaming, which may not include messages appended by the