Skip to content

fix(a2a): inactivity_action events run to completion on client disconnect - #76

Open
mateusbellozupko wants to merge 3 commits into
evolution-foundation:mainfrom
mateusbellozupko:fix/inactivity-action-disconnect-cancellation
Open

mateusbellozupko wants to merge 3 commits into
evolution-foundation:mainfrom
mateusbellozupko:fix/inactivity-action-disconnect-cancellation

Conversation

@mateusbellozupko

@mateusbellozupko mateusbellozupko commented Sep 26, 2026 •

Copy link
Copy Markdown

Root cause

Rails fires inactivity_action events from a Sidekiq job that completes in ~80ms. Because the HTTP connection drops as soon as Sidekiq finishes dispatching, run_unless_client_disconnects always detected "client disconnected" and cancelled the LLM call before any message was sent to the lead. The execution record was already committed in Rails (rule marked done), but no WhatsApp message ever reached the lead.

Fix

  • src/utils/client_disconnect.py — adds ignore_disconnect: bool = False to run_unless_client_disconnects. When True, the cancel-on-disconnect watcher is bypassed entirely and the coroutine runs to completion.
  • src/api/a2a_routes.py — detects metadata.evoai_crm_event == "inactivity_action" at the call site and passes ignore_disconnect=True for those events.

Behaviour

Event type Before After
Interactive bot conversation Cancel on disconnect (CRM-236) Unchanged
inactivity_action (fire-and-forget) Cancelled in ~80ms, no message sent Runs to completion

Related

  • Fixes silent failure introduced by CRM-236 for system-initiated events
  • Related: stage inactivity rules on evo-ai-crm-community

Summary by Sourcery

Ensure fire-and-forget system-initiated agent events run to completion without changing disconnect handling for interactive conversations.

Bug Fixes:

  • Allow system-initiated A2A events, including inactivity actions, to complete after the short-lived HTTP caller disconnects, preventing silent loss of outbound messages.

Enhancements:

  • Preserve client-disconnect cancellation for interactive bot conversations while exempting fire-and-forget system events.

…nect

Rails fires-and-forgets inactivity_action events from a Sidekiq job that
completes in ~80ms. The processor always detects "client disconnected"
and was cancelling the LLM call before any message reached the lead.

Add ignore_disconnect=True path to run_unless_client_disconnects so
system-initiated events complete regardless of HTTP connection lifetime.
Interactive bot conversations are unaffected (CRM-236 behaviour preserved).
@sourcery-ai

sourcery-ai Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

The PR prevents inactivity_action processing from being cancelled when Rails’ short-lived Sidekiq request disconnects: the disconnect helper now supports an opt-in bypass, and the A2A route enables it only for inactivity_action events, preserving cancellation for interactive conversations.

Sequence diagram for inactivity action completion after client disconnect

sequenceDiagram
    participant Rails as Rails Sidekiq
    participant A2A as A2A route
    participant Helper as run_unless_client_disconnects
    participant Agent as run_agent
    participant Lead as WhatsApp lead

    Rails->>A2A: handle_message_send(metadata)
    A2A->>A2A: metadata.get(evoai_crm_event)
    A2A->>Helper: run_unless_client_disconnects(ignore_disconnect=True)
    A2A--xRails: HTTP connection disconnects
    Helper->>Agent: await run_agent()
    Agent->>Lead: Send inactivity action message
    Agent-->>Helper: final_response
    Helper-->>A2A: result
Loading

Flow diagram for event-specific disconnect handling

flowchart TD
    A[handle_message_send] --> B{evoai_crm_event == inactivity_action}
    B -->|Yes| C["run_unless_client_disconnects(ignore_disconnect=True)"]
    B -->|No| D["run_unless_client_disconnects(ignore_disconnect=False)"]
    C --> E[run_agent runs to completion]
    D --> F{Client disconnects first}
    F -->|Yes| G[Cancel agent execution]
    F -->|No| E
Loading

File-Level Changes

Change Details Files
Allow selected system-initiated work to finish after the HTTP client disconnects.
  • Added an opt-in flag that bypasses the disconnect watcher while preserving existing cancellation behavior by default.
  • Awaited the coroutine directly when bypassing cancellation.
src/utils/client_disconnect.py
Apply completion semantics specifically to inactivity actions.
  • Detected the inactivity-action CRM event in the A2A message handler.
  • Passed the opt-in flag for fire-and-forget inactivity actions while leaving interactive conversations unchanged.
src/api/a2a_routes.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/utils/client_disconnect.py" line_range="131" />
<code_context>
     task = asyncio.ensure_future(coro)

-    if not cancel_on_disconnect_enabled():
+    if not cancel_on_disconnect_enabled() or ignore_disconnect:
         return await task

</code_context>
<issue_to_address>
**nitpick:** The module-level documentation says the utility stops an agent once nobody is waiting for its answer, but the new `ignore_disconnect` path intentionally continues running after the caller disconnects. The documentation is now misleading for the supported inactivity-action behavior.

**Triggers:** When maintainers rely on the module docstring to understand the utility's behavior.

**Suggested fix:** Update the module-level docstring to document the intentional fire-and-forget exception for system-initiated work.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. If this exception is wrong, an inactivity-triggered agent run will continue after the client disconnects and may write records or perform downstream actions that the caller would previously have prevented by hanging up. Reverting stops future runs from being detached, but any side effects from runs already completed would need to be repaired or undone separately.


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/utils/client_disconnect.py
…get callers

conversation_opened, conversation_resolved and webwidget_triggered are sent
from AgentBotListener via HttpRequestJob (Sidekiq) — the job disconnects as
soon as the HTTP handshake completes, not when the LLM finishes. Without
this exemption CRM-236 would silently cancel those agent runs too.

Live bot conversation events (message_created, message_updated) are excluded:
their caller is the bot runtime, which holds the connection until the reply is
sent — disconnect there is a real timeout that should cancel the run.
…t exception

Sourcery nitpick on PR evolution-foundation#76: the module docstring said the utility always
stops an agent once nobody is waiting for the answer, but ignore_disconnect
now lets system-initiated work (e.g. inactivity_action) run to completion
after the caller disconnects. Document the exception.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant