1717
1818from slackbot ._internal .constants import WORKSPACE_TO_CHANNEL_ID
1919from slackbot ._internal .templates import CHANNEL_REDIRECT_MESSAGE , WELCOME_MESSAGE
20+ from slackbot ._internal .thread_status import (
21+ get_status as get_thread_status ,
22+ )
23+ from slackbot ._internal .thread_status import (
24+ mark_completed as mark_thread_completed ,
25+ )
26+ from slackbot ._internal .thread_status import (
27+ try_acquire as try_acquire_thread ,
28+ )
2029from slackbot .assets import summarize_thread
2130from slackbot .core import (
2231 Database ,
4049
4150logger = get_logger (__name__ )
4251
43- # In-process thread guard to avoid duplicate handling on edits/rapid events
44- _thread_locks : dict [ str , asyncio . Lock ] = {}
52+ # Duplicate handling is coordinated via a small SQLite table in
53+ # _internal.thread_status to work across processes.
4554
4655
4756def get_designated_channel_for_workspace (team_id : str ) -> str | None :
@@ -143,11 +152,11 @@ async def handle_message(payload: SlackPayload, db: Database):
143152 # Use the root thread timestamp as our idempotency key
144153 root_ts = thread_ts
145154
146- # Acquire per-thread lock; if already locked, another handler is running
147- lock = _thread_locks . setdefault ( root_ts , asyncio . Lock () )
148- if lock . locked () :
149- # If this is an edit to the original message, politely ignore
150- if event . subtype == "message_changed " :
155+ # Cross-process acquire; only one handler should proceed
156+ acquired = await try_acquire_thread ( db , root_ts )
157+ if not acquired :
158+ status = await get_thread_status ( db , root_ts )
159+ if status == "in_progress " :
151160 assert event .channel is not None , "No channel found"
152161 await post_slack_message (
153162 message = (
@@ -163,9 +172,8 @@ async def handle_message(payload: SlackPayload, db: Database):
163172 name = "IGNORED_EDIT" ,
164173 data = dict (thread_ts = root_ts ),
165174 )
166- # Otherwise, just skip duplicate events while busy
167175 return Completed (
168- message = "Skipped duplicate event while in progress " ,
176+ message = "Duplicate event after completion " ,
169177 name = "SKIPPED_DUPLICATE" ,
170178 data = dict (thread_ts = root_ts ),
171179 )
@@ -219,49 +227,47 @@ async def handle_message(payload: SlackPayload, db: Database):
219227 bot_id = bot_user_id or "unknown" ,
220228 )
221229
222- async with lock :
223- try :
224- result = await run_agent (
225- cleaned_message ,
226- conversation ,
227- user_context ,
228- event .channel ,
229- thread_ts ,
230- ) # type: ignore
231-
232- await db .add_thread_messages (thread_ts , result .new_messages ())
233- conversation .extend (result .new_messages ())
234- assert event .channel is not None , "No channel found"
235- await task (post_slack_message )(
236- message = result .output ,
237- channel_id = event .channel ,
238- thread_ts = thread_ts ,
239- )
240- except Exception as e :
241- logger .error (f"Error running agent: { e } " )
242- assert event .channel is not None , "No channel found"
243- await task (post_slack_message )(
244- message = "Sorry, I encountered an error while processing your request. Please try again." ,
245- channel_id = event .channel ,
246- thread_ts = thread_ts ,
247- )
248- # Still return completed so we don't retry
249- return Completed (
250- message = "Error during agent execution" ,
251- name = "ERROR_HANDLED" ,
252- data = dict (error = str (e ), user_context = user_context ),
253- )
254- finally :
255- # Clean up lock map to avoid unbounded growth
256- # Only pop if this lock is the same instance
257- current = _thread_locks .get (root_ts )
258- if current is lock :
259- _thread_locks .pop (root_ts , None )
260-
230+ try :
231+ result = await run_agent (
232+ cleaned_message ,
233+ conversation ,
234+ user_context ,
235+ event .channel ,
236+ thread_ts ,
237+ ) # type: ignore
238+
239+ await db .add_thread_messages (thread_ts , result .new_messages ())
240+ conversation .extend (result .new_messages ())
241+ assert event .channel is not None , "No channel found"
242+ await task (post_slack_message )(
243+ message = result .output ,
244+ channel_id = event .channel ,
245+ thread_ts = thread_ts ,
246+ )
247+ except Exception as e :
248+ logger .error (f"Error running agent: { e } " )
249+ assert event .channel is not None , "No channel found"
250+ await task (post_slack_message )(
251+ message = "Sorry, I encountered an error while processing your request. Please try again." ,
252+ channel_id = event .channel ,
253+ thread_ts = thread_ts ,
254+ )
255+ # Still return completed so we don't retry
261256 return Completed (
262- message = "Responded to mention" ,
263- data = dict (user_context = user_context , conversation = conversation ),
257+ message = "Error during agent execution" ,
258+ name = "ERROR_HANDLED" ,
259+ data = dict (error = str (e ), user_context = user_context ),
264260 )
261+ finally :
262+ try :
263+ await mark_thread_completed (db , root_ts )
264+ except Exception :
265+ logger .warning ("Failed to mark thread as completed" )
266+
267+ return Completed (
268+ message = "Responded to mention" ,
269+ data = dict (user_context = user_context , conversation = conversation ),
270+ )
265271
266272 return Completed (message = "Skipping non-mention" , name = "SKIPPED" )
267273
0 commit comments