Skip to content

Commit 06be2f5

Browse files
committed
slackbot: ignore edits while initial response is in progress (in-process lock)\n\n- Add simple per-thread asyncio.Lock to gate concurrent handling\n- Send friendly notice for message_changed edits during processing\n- Avoid DB changes; keep example lightweight\n- Add subtype to SlackEvent model\n
1 parent 8dc1e91 commit 06be2f5

3 files changed

Lines changed: 78 additions & 29 deletions

File tree

examples/slackbot/src/slackbot/api.py

Lines changed: 74 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040

4141
logger = get_logger(__name__)
4242

43+
# In-process thread guard to avoid duplicate handling on edits/rapid events
44+
_thread_locks: dict[str, asyncio.Lock] = {}
45+
4346

4447
def get_designated_channel_for_workspace(team_id: str) -> str | None:
4548
"""Get the designated channel ID for a given workspace team ID."""
@@ -137,6 +140,36 @@ async def handle_message(payload: SlackPayload, db: Database):
137140
return Completed(message="Message too long", name="SKIPPED")
138141

139142
if re.search(BOT_MENTION, user_message) and payload.authorizations:
143+
# Use the root thread timestamp as our idempotency key
144+
root_ts = thread_ts
145+
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":
151+
assert event.channel is not None, "No channel found"
152+
await post_slack_message(
153+
message=(
154+
"✋ I noticed you edited your original message. "
155+
"I'm already working on your first version — please add any "
156+
"clarifications as new messages in this thread so I don't lose track."
157+
),
158+
channel_id=event.channel,
159+
thread_ts=root_ts,
160+
)
161+
return Completed(
162+
message="Ignored edit while in progress",
163+
name="IGNORED_EDIT",
164+
data=dict(thread_ts=root_ts),
165+
)
166+
# Otherwise, just skip duplicate events while busy
167+
return Completed(
168+
message="Skipped duplicate event while in progress",
169+
name="SKIPPED_DUPLICATE",
170+
data=dict(thread_ts=root_ts),
171+
)
172+
140173
# Check if this is the designated channel
141174
team_id = payload.team_id or ""
142175
is_designated = check_if_designated_channel(event.channel, team_id)
@@ -186,37 +219,49 @@ async def handle_message(payload: SlackPayload, db: Database):
186219
bot_id=bot_user_id or "unknown",
187220
)
188221

189-
try:
190-
result = await run_agent(
191-
cleaned_message, conversation, user_context, event.channel, thread_ts
192-
) # type: ignore
193-
194-
await db.add_thread_messages(thread_ts, result.new_messages())
195-
conversation.extend(result.new_messages())
196-
assert event.channel is not None, "No channel found"
197-
await task(post_slack_message)(
198-
message=result.output,
199-
channel_id=event.channel,
200-
thread_ts=thread_ts,
201-
)
202-
except Exception as e:
203-
logger.error(f"Error running agent: {e}")
204-
assert event.channel is not None, "No channel found"
205-
await task(post_slack_message)(
206-
message="Sorry, I encountered an error while processing your request. Please try again.",
207-
channel_id=event.channel,
208-
thread_ts=thread_ts,
209-
)
210-
# Still return completed so we don't retry
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+
211261
return Completed(
212-
message="Error during agent execution",
213-
name="ERROR_HANDLED",
214-
data=dict(error=str(e), user_context=user_context),
262+
message="Responded to mention",
263+
data=dict(user_context=user_context, conversation=conversation),
215264
)
216-
return Completed(
217-
message="Responded to mention",
218-
data=dict(user_context=user_context, conversation=conversation),
219-
)
220265

221266
return Completed(message="Skipping non-mention", name="SKIPPED")
222267

examples/slackbot/src/slackbot/core.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ def _insert():
129129

130130
await self.loop.run_in_executor(self.executor, _insert)
131131

132+
# Note: Thread status tracking is handled in-process within api.py to keep
133+
# persistence minimal for the examples package.
134+
132135

133136
@task(task_run_name="build user context for {user_id}")
134137
def build_user_context(

examples/slackbot/src/slackbot/slack.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class EventBlock(BaseModel):
2929
class SlackEvent(BaseModel):
3030
client_msg_id: str | None = None
3131
type: str
32+
subtype: str | None = None
3233
text: str | None = None
3334
user: str | dict[str, Any] | None = None
3435
ts: str | None = None

0 commit comments

Comments
 (0)