Skip to content

fix: restore _delivery.py removed while still imported by durable delivery#2129

Merged
MervinPraison merged 1 commit into
mainfrom
cursor/critical-bug-investigation-3971
Jun 22, 2026
Merged

fix: restore _delivery.py removed while still imported by durable delivery#2129
MervinPraison merged 1 commit into
mainfrom
cursor/critical-bug-investigation-3971

Conversation

@cursor

@cursor cursor Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Bug and impact

PR #2054 (fix: remove three orphaned wrapper modules) deleted bots/_delivery.py, believing it was superseded by bots/delivery.py. Those modules are unrelated:

  • delivery.pyDeliveryRouter / ChannelDirectory (proactive routing)
  • _delivery.pyDurableDelivery, deliver_with_retry, deliver_chunked (crash-safe outbound delivery)

_durable_adapter.py and bots/__init__.py still import from ._delivery, so any code path that enables durable outbound delivery crashes with ModuleNotFoundError. This breaks crash-safe channel reply delivery added in PR #2036.

Concrete trigger: from praisonai.bots import DurableDelivery or enabling DurableAdapterMixin.setup_durable_delivery(outbox_path=...).

Root cause

Dead-code removal grep only checked for UnifiedDelivery / create_delivery call sites and missed active imports of DurableDelivery and related exports.

Fix

  • Restore bots/_delivery.py from the pre-deletion revision
  • Add tests/unit/bots/test_delivery_imports.py regression tests for the public exports

Validation

  • python3 -c "from praisonai.bots import DurableDelivery" succeeds
  • pytest tests/unit/bots/test_delivery_imports.py — 5 passed
Open in Web View Automation 

Summary by CodeRabbit

  • New Features

    • Added adaptive message delivery with support for typing indicators and automatic message chunking for long content.
    • Implemented retry mechanism with exponential backoff for failed message deliveries.
    • Added durable delivery layer with message persistence and queue processing for reliable message handling.
  • Tests

    • Added unit tests for delivery system exports and functionality.

@MervinPraison

Copy link
Copy Markdown
Owner

@coderabbitai review

@MervinPraison

Copy link
Copy Markdown
Owner

/review

@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f32fb5de-eb1d-473e-937a-b0e38d62e104

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new _delivery.py module implementing a complete async message delivery layer for PraisonAI bots: a MessageSender protocol, UnifiedDelivery for platform-adaptive chunking/streaming, deliver_with_retry with exponential backoff, deliver_chunked, DurableDelivery with outbox persistence and drain, and a regression test for public exports.

Changes

Bot Delivery Layer

Layer / File(s) Summary
MessageSender protocol, module setup, and factory
src/praisonai/praisonai/bots/_delivery.py
Defines module-level imports, logging, the public MessageSender async protocol, the create_delivery factory, and the __all__ export list.
UnifiedDelivery: platform-adaptive send and streaming
src/praisonai/praisonai/bots/_delivery.py
Implements UnifiedDelivery.send_message dispatching typing indicators, progressive streaming/editing via _send_streamed for long messages on capable platforms, and chunked fallback with per-chunk rate limiting. Adds _show_typing and _acquire_rate_limit helpers.
deliver_with_retry and deliver_chunked
src/praisonai/praisonai/bots/_delivery.py
Adds deliver_with_retry with bounded exponential backoff, permanent-vs-recoverable error classification, abort signal cancellation, and per-attempt logging returning (success, error_message). Adds deliver_chunked scoping reply_to to the first chunk only.
DurableDelivery: outbox persistence and drain
src/praisonai/praisonai/bots/_delivery.py
Implements DurableDelivery.send enqueuing payloads with idempotency keys and marking outbox entries as sent or permanently/transiently failed. Implements drain_pending to process queued items with single-attempt delivery and permanent-failure escalation via raise.
Export and wiring regression tests
src/praisonai/tests/unit/bots/test_delivery_imports.py
Parameterized test checks praisonai.bots exports for durable-delivery symbols; wiring test confirms DurableAdapterMixin.setup_durable_delivery(outbox_path=None) sets durable_delivery to None.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • MervinPraison/PraisonAI#2036: Introduces the same durable, idempotent outbound delivery mechanics (deliver_with_retry, DurableDelivery, DurableAdapterMixin, outbox drain) that this PR implements.
  • MervinPraison/PraisonAI#2054: Removed _delivery.py as dead code; this PR reintroduces and fully implements that same module.

Suggested reviewers

  • MervinPraison

Poem

🐇 A message hops from bot to chat,
With chunking, streaming — fancy that!
Retry with backoff, typed and queued,
The outbox drained, no message skewed.
Idempotent keys keep things neat,
This rabbit's delivery is complete! 📨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: restoring a previously deleted module (_delivery.py) that is still being imported, fixing a critical module dependency bug.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/critical-bug-investigation-3971

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@MervinPraison

Copy link
Copy Markdown
Owner

@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above from Qodo, Coderabbit, and Gemini first — incorporate their findings.

Review areas:

  1. Bloat check: Are changes minimal and focused? Any unnecessary code or scope creep?
  2. Security: Any hardcoded secrets, unsafe eval/exec, missing input validation?
  3. Performance: Any module-level heavy imports? Hot-path regressions?
  4. Tests: Are tests included? Do they cover the changes adequately?
  5. Backward compat: Any public API changes without deprecation?
  6. Code quality: DRY violations, naming conventions, error handling?
  7. Address reviewer feedback: If Qodo, Coderabbit, or Gemini flagged valid issues, include them in your review
  8. Suggest specific improvements with code examples where possible

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/praisonai/praisonai/bots/_delivery.py (2)

89-89: 💤 Low value

Store the task reference to prevent premature garbage collection.

The task created for typing indicator is not stored, which could lead to the task being garbage collected before completion if no other references exist. While _show_typing handles exceptions internally, storing the reference is the recommended pattern.

♻️ Suggested fix
         # Show typing indicator if supported
         if typing and caps.supports_typing:
-            asyncio.create_task(self._show_typing(channel_id))
+            self._typing_task = asyncio.create_task(self._show_typing(channel_id))

Add the instance variable in __init__:

self._typing_task: Optional[asyncio.Task] = None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai/praisonai/bots/_delivery.py` at line 89, The
asyncio.create_task call for self._show_typing(channel_id) at line 89 does not
store the returned task reference, which can cause premature garbage collection.
Add an instance variable self._typing_task of type Optional[asyncio.Task]
initialized to None in the __init__ method, and then assign the result of
asyncio.create_task(self._show_typing(channel_id)) to self._typing_task instead
of discarding it. This ensures the task reference is maintained and prevents it
from being garbage collected before completion.

Source: Linters/SAST tools


503-504: ⚡ Quick win

Fragile string-based permanent error detection.

The permanent error check relies on matching the exact prefix "Permanent error:" from deliver_with_retry (line 323). This coupling is fragile—if the error message format changes, this detection silently fails and permanent errors get retried indefinitely.

Consider using a structured return value or a sentinel constant for error classification instead of string matching.

♻️ Suggested approach

Define a constant for the prefix and use it in both places:

# At module level
PERMANENT_ERROR_PREFIX = "Permanent error:"

# In deliver_with_retry (line 323)
return False, f"{PERMANENT_ERROR_PREFIX} {last_error}"

# In DurableDelivery.send (line 503)
permanent = error and error.startswith(PERMANENT_ERROR_PREFIX)

Or use a structured return like DeliveryResult(success: bool, error: Optional[str], permanent: bool).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai/praisonai/bots/_delivery.py` around lines 503 - 504, The
permanent error detection at the mark_failed call relies on substring matching
"Permanent error:" which is fragile and tightly coupled to the error message
format in deliver_with_retry. Extract the error prefix into a module-level
constant (define it near the top of the file), then update the permanent error
check to use startswith() with this constant instead of the in operator, and
ensure the deliver_with_retry function also uses this same constant when
constructing its error message so that both locations reference the same value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/praisonai/praisonai/bots/_delivery.py`:
- Line 89: The asyncio.create_task call for self._show_typing(channel_id) at
line 89 does not store the returned task reference, which can cause premature
garbage collection. Add an instance variable self._typing_task of type
Optional[asyncio.Task] initialized to None in the __init__ method, and then
assign the result of asyncio.create_task(self._show_typing(channel_id)) to
self._typing_task instead of discarding it. This ensures the task reference is
maintained and prevents it from being garbage collected before completion.
- Around line 503-504: The permanent error detection at the mark_failed call
relies on substring matching "Permanent error:" which is fragile and tightly
coupled to the error message format in deliver_with_retry. Extract the error
prefix into a module-level constant (define it near the top of the file), then
update the permanent error check to use startswith() with this constant instead
of the in operator, and ensure the deliver_with_retry function also uses this
same constant when constructing its error message so that both locations
reference the same value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cf9e037a-c755-4390-8d14-9242947062ae

📥 Commits

Reviewing files that changed from the base of the PR and between 5f39d66 and 34e4c63.

📒 Files selected for processing (2)
  • src/praisonai/praisonai/bots/_delivery.py
  • src/praisonai/tests/unit/bots/test_delivery_imports.py

@MervinPraison

Copy link
Copy Markdown
Owner

@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.

Phase 1: Review per AGENTS.md

  1. Protocol-driven: check heavy implementations vs core SDK
  2. Backward compatible: ensure zero feature regressions
  3. Performance: no hot-path regressions

Phase 2: FIX Valid Issues
4. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix
5. Push all code fixes directly to THIS branch (do NOT create a new PR)
6. Comment a summary of exact files modified and what you skipped

Phase 3: Final Verdict
7. If all issues are resolved, approve the PR / close the Issue
8. If blocking issues remain, request changes / leave clear action items

@praisonai-triage-agent

praisonai-triage-agent Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MervinPraison's task —— View job


Architecture Review Complete ✅

Phase 1: Architecture Review

Protocol-driven design: The module properly imports protocols and capabilities from TYPE_CHECKING. Heavy implementations like UnifiedDelivery and DurableDelivery are in the wrapper layer (praisonai/), not in core SDK.

Backward compatibility: The PR correctly restores a module that was still being imported. No API changes, just restoring existing functionality. All imports verified working.

Performance: Uses lazy imports with TYPE_CHECKING, no hot-path regressions identified. No heavy module-level imports.

Phase 2: Fixed Issues

Implemented both fixes identified by CodeRabbit:

  1. Line 89: Added self._typing_task instance variable to store asyncio task reference, preventing premature garbage collection
  2. Lines 503-504, 323, 552: Extracted PERMANENT_ERROR_PREFIX constant and replaced fragile string matching with startswith() for robust error detection

Files Modified

  • src/praisonai/praisonai/bots/_delivery.py: Added typing task storage and error constant

Phase 3: Final Verdict ✅

APPROVED - This PR correctly fixes the critical bug where _delivery.py was incorrectly deleted while still being imported. The restoration:

  • Maintains backward compatibility (no API changes)
  • Follows protocol-driven architecture (heavy implementations in wrapper)
  • Includes regression tests
  • Addresses all valid review feedback

The changes are minimal, focused, and necessary to fix the ModuleNotFoundError that breaks durable delivery functionality.


@MervinPraison MervinPraison marked this pull request as ready for review June 22, 2026 22:18
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@MervinPraison

Copy link
Copy Markdown
Owner

@claude Merge conflict resolution audit (fresh rebase onto origin/main @ 157c4282e, 2026-06-22)

Context: main already restored bots/_delivery.py in #2102 (81c2cf8e8). This branch still carries the original “restore deleted module” commits and is 50 commits behind / 2 commits ahead of main. Rebase stops on commit 1/2 with an add/add conflict on the same file.


Merge & CI status

Field Value
mergeStateStatus DIRTY
mergeable CONFLICTING
Branch cursor/critical-bug-investigation-3971
Draft No

CI summary (latest rollup)

Check Result
Core Tests (test-core) SUCCESS
Optimized Test Suite (smoke, main (3.11), openai-live, test-summary) SUCCESS
Windows Tests (test-windows) SUCCESS
GitGuardian Security Checks SUCCESS
CodeRabbit SUCCESS
PraisonAI PR Reviewer (review) SKIPPED
Extended provider jobs SKIPPED
Mintlify Deployment SKIPPED

CI is green on the pre-merge branch tip, but GitHub reports merge conflicts with current main — rebase is required before merge.


Conflict inventory (rebase origin/main)

File Conflict hunks Type
src/praisonai/praisonai/bots/_delivery.py 10 add/add (both sides added the file independently)

No other files reached conflict during rebase (failure on commit 34e4c6341).


src/praisonai/praisonai/bots/_delivery.py

Hunk 1 — typing task lifetime (UnifiedDelivery.__init__)

main (HEAD during rebase):

        self._capabilities: Optional["PlatformCapabilities"] = None
        self._rate_limiter: Optional[RateLimiter] = None
        self._streamer: Optional[DraftStreamer] = None
        self._background_tasks: set[asyncio.Task] = set()

PR side (34e4c6341): omits _background_tasks (first commit had fire-and-forget typing; second commit 34e7ee0c0 adds _typing_task instead).

Guidance: Keep main’s _background_tasks set (stronger GC/lifetime than a single _typing_task or fire-and-forget). If you port anything from the PR follow-up commit, prefer retaining _background_tasks unless you deliberately unify on one pattern.


Hunk 2 — start typing indicator

main:

        if typing and caps.supports_typing:
            task = asyncio.create_task(self._show_typing(channel_id))
            self._background_tasks.add(task)
            task.add_done_callback(self._background_tasks.discard)

PR (34e4c6341):

        if typing and caps.supports_typing:
            asyncio.create_task(self._show_typing(channel_id))

PR tip (34e7ee0c0 on branch, not yet applied during failed rebase):

        if typing and caps.supports_typing:
            self._typing_task = asyncio.create_task(self._show_typing(channel_id))

Guidance: Take main (background task tracking). Do not regress to fire-and-forget.


Hunks 3, 5, 7, 8 (partial) — rate limiting calls

main:

            if caps.needs_rate_limit:
                await self._acquire_rate_limit(channel_id)

PR:

            if caps.needs_rate_limit:
                await self._acquire_rate_limit()

Guidance: Keep main — per-channel rate limiting is intentional on latest main.


Hunks 4 & 6 — streaming completion flag

main:

        streaming_completed = False
        while current_pos < len(text):
            ...
            if next_pos >= len(text):
                streaming_completed = True
                break

PR: removes streaming_completed tracking.

Guidance: Keep main’s streaming_completed logic; it avoids duplicate final edits when streaming already completed.


Hunk 8 — final edit vs elif not streaming_completed

main:

        elif not streaming_completed:
            # Final edit with complete content (fits in limit and wasn't already sent)
            await asyncio.sleep(caps.edit_interval_ms / 1000.0)
            if caps.needs_rate_limit:
                await self._acquire_rate_limit(channel_id)
            await self.bot.edit_message(...)

PR:

        else:
            # Final edit with complete content (fits in limit)
            ...
                await self._acquire_rate_limit()

Guidance: Keep main’s elif not streaming_completed branch and channel_id rate-limit calls.


Hunks 9–10 — _acquire_rate_limit signature

main:

    async def _acquire_rate_limit(self, channel_id: Optional[str] = None) -> None:
        """Acquire rate limit token if platform needs it.
        Args:
            channel_id: Optional channel ID for per-channel limiting
        """
        ...
            await self._rate_limiter.acquire(channel_id)

PR:

    async def _acquire_rate_limit(self) -> None:
        ...
            await self._rate_limiter.acquire()

Guidance: Keep main signature and acquire(channel_id).


Non-conflicting value from this PR (port onto main base)

PR branch tip adds (worth keeping if not already on main):

PERMANENT_ERROR_PREFIX = "Permanent error:"
...
return False, f"{PERMANENT_ERROR_PREFIX} {last_error}"
...
permanent = error and error.startswith(PERMANENT_ERROR_PREFIX)
...
if not success and error and error.startswith(PERMANENT_ERROR_PREFIX):

main still uses inline "Permanent error:" strings and "Permanent error:" in error — port the constant + startswith pattern from commit 34e7ee0c0 after taking main’s file as the conflict base.


Tests only on this PR branch

src/praisonai/tests/unit/bots/test_delivery_imports.py exists on the PR branch but not on main. After rebase, re-add this file unchanged (or equivalent coverage) so the #2054 regression stays covered.


Recommended resolution strategy

  1. Rebase onto current main: git fetch origin main && git rebase origin/main
  2. For _delivery.py add/add conflict: git checkout --ours src/praisonai/praisonai/bots/_delivery.py during rebase (ours = updated main), then manually apply:
    • PERMANENT_ERROR_PREFIX + startswith usages from 34e7ee0c0
    • Restore test_delivery_imports.py from the PR branch
  3. Do not re-introduce the full pre-fix: gateway session data loss and restore deleted _delivery module #2102 module from 34e4c6341 — it diverges on rate limiting and streaming.
  4. Continue rebase; verify commit 2/2 (34e7ee0c0) — it may become empty or need manual squashing into the port above.
  5. Run: pytest src/praisonai/tests/unit/bots/test_delivery_imports.py and relevant bot delivery tests.
  6. Push: git push --force-with-lease origin cursor/critical-bug-investigation-3971
  7. Re-run / wait for CI on the rebased tip and confirm GitHub shows mergeable.

Action items for @claude

  1. Rebase onto origin/main and resolve all 10 hunks in _delivery.py as above (main base + port constant/tests).
  2. Push with --force-with-lease.
  3. Fix any CI regressions from the 50-commit drift (expect none if scope stays minimal).
  4. Post a final verdict comment after the rebased branch is mergeable:

Interim verdict (pre-rebase): Blocked — conflicting with main. The original bug (missing _delivery.py) is already fixed on main; this PR should shrink to a targeted port of review fixes + tests, not a full module re-add.

@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR restores bots/_delivery.py that was mistakenly deleted in PR #2054 and adds a minor refactor (a named constant for the permanent-error prefix) along with regression tests to prevent the same accident from recurring.

  • _delivery.py is restored with a PERMANENT_ERROR_PREFIX = \"Permanent error:\" constant extracted from two repeated string literals, and .startswith() replaces a substring in check for a more precise match.
  • tests/unit/bots/test_delivery_imports.py adds parametrized import tests for all four affected public symbols and a smoke test that exercises DurableAdapterMixin.setup_durable_delivery with a None outbox path.

Confidence Score: 5/5

Safe to merge — the change restores a deleted module that was still actively imported and the only code modifications are a constant extraction and a more precise string-prefix check.

Both files are straightforward: the module restoration is correct, the PERMANENT_ERROR_PREFIX constant produces the same string as the previous literals, and switching from a substring in check to startswith is strictly more precise without changing observable behavior. The regression tests correctly cover all four previously broken public exports.

No files require special attention.

Important Files Changed

Filename Overview
src/praisonai/praisonai/bots/_delivery.py Restored module with a clean constant extraction; the PERMANENT_ERROR_PREFIX refactor is correct and the startswith change is a strict improvement over the previous substring check.
src/praisonai/tests/unit/bots/test_delivery_imports.py New regression test file covering imports and a basic instantiation smoke test; tests are accurate and will catch the original deletion regression.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller
    participant DurableDelivery
    participant deliver_with_retry
    participant OutboundQueue
    participant Adapter

    Caller->>DurableDelivery: send(channel_id, content)
    DurableDelivery->>OutboundQueue: enqueue(idempotency_key, target, payload)
    OutboundQueue-->>DurableDelivery: key
    DurableDelivery->>deliver_with_retry: deliver(adapter, channel_id, content)
    deliver_with_retry->>Adapter: send_message(channel_id, content)
    alt success
        Adapter-->>deliver_with_retry: ok
        deliver_with_retry-->>DurableDelivery: (True, None)
        DurableDelivery->>OutboundQueue: mark_sent(key)
    else permanent error (startswith PERMANENT_ERROR_PREFIX)
        Adapter-->>deliver_with_retry: raises non-recoverable error
        deliver_with_retry-->>DurableDelivery: (False, "Permanent error: ...")
        DurableDelivery->>OutboundQueue: "mark_failed(key, permanent=True)"
    else transient failure
        Adapter-->>deliver_with_retry: raises recoverable error
        deliver_with_retry-->>DurableDelivery: (False, "Max attempts exceeded: ...")
        DurableDelivery->>OutboundQueue: "mark_failed(key, permanent=False)"
    end
    DurableDelivery-->>Caller: success (bool)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Caller
    participant DurableDelivery
    participant deliver_with_retry
    participant OutboundQueue
    participant Adapter

    Caller->>DurableDelivery: send(channel_id, content)
    DurableDelivery->>OutboundQueue: enqueue(idempotency_key, target, payload)
    OutboundQueue-->>DurableDelivery: key
    DurableDelivery->>deliver_with_retry: deliver(adapter, channel_id, content)
    deliver_with_retry->>Adapter: send_message(channel_id, content)
    alt success
        Adapter-->>deliver_with_retry: ok
        deliver_with_retry-->>DurableDelivery: (True, None)
        DurableDelivery->>OutboundQueue: mark_sent(key)
    else permanent error (startswith PERMANENT_ERROR_PREFIX)
        Adapter-->>deliver_with_retry: raises non-recoverable error
        deliver_with_retry-->>DurableDelivery: (False, "Permanent error: ...")
        DurableDelivery->>OutboundQueue: "mark_failed(key, permanent=True)"
    else transient failure
        Adapter-->>deliver_with_retry: raises recoverable error
        deliver_with_retry-->>DurableDelivery: (False, "Max attempts exceeded: ...")
        DurableDelivery->>OutboundQueue: "mark_failed(key, permanent=False)"
    end
    DurableDelivery-->>Caller: success (bool)
Loading

Reviews (2): Last reviewed commit: "fix: restore _delivery.py removed while ..." | Re-trigger Greptile


import asyncio
import logging
from typing import TYPE_CHECKING, Optional, Union, Dict, Any, List, Awaitable, Callable, Protocol

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Awaitable and Callable are imported but never referenced anywhere in this file. Removing them keeps the import line clean and avoids lint warnings.

Suggested change
from typing import TYPE_CHECKING, Optional, Union, Dict, Any, List, Awaitable, Callable, Protocol
from typing import TYPE_CHECKING, Optional, Union, Dict, Any, List, Protocol

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +376 to +378
if len(content) <= max_length:
await adapter.send_message(channel_id, content, **send_kwargs)
return 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 deliver_chunked guards the fast path with len(content) (character count) but chunk_message can operate in other length units (e.g. bytes). When a caller passes a max_length tuned for a byte-counting platform, a message whose len() is below the limit may still be over-limit in bytes, so chunk_message would split it — but the leading return 1 branch would have already sent it unsplit via a single send_message call, potentially exceeding the platform's limit. Consider using _calculate_length with an explicit length_unit here, or at minimum document that this function always counts in characters.

Suggested change
if len(content) <= max_length:
await adapter.send_message(channel_id, content, **send_kwargs)
return 1
if _calculate_length(content, "chars") <= max_length:
await adapter.send_message(channel_id, content, **send_kwargs)
return 1

Comment on lines +567 to +568
"DurableDelivery",
] No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The file is missing a trailing newline. Most linters and POSIX tools expect text files to end with .

Suggested change
"DurableDelivery",
]
"DurableDelivery",
]

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@praisonai-triage-agent

praisonai-triage-agent Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MervinPraison's task —— View job


✅ Merge Conflict Resolution Complete

Successfully rebased the branch onto the latest main and resolved all conflicts.

Todo List:

  • Setup git identity and authentication
  • Fetch latest main and start rebase
  • Resolve add/add conflict on _delivery.py
  • Port improvements from PR (PERMANENT_ERROR_PREFIX constant)
  • Complete rebase and force-push
  • Run tests to verify changes
  • Provide final verdict on PR value

Changes Summary

After rebasing onto main (which already restored _delivery.py in PR #2102), this PR now contains only the following valuable improvements:

  1. Added PERMANENT_ERROR_PREFIX constant - Replaces inline string literals for better maintainability
  2. Improved error detection - Uses startswith() instead of in operator for more robust permanent error detection
  3. Added regression test - test_delivery_imports.py ensures the public exports remain available

Files Modified

  • src/praisonai/praisonai/bots/_delivery.py - Added constant and improved error detection (4 lines changed)
  • src/praisonai/tests/unit/bots/test_delivery_imports.py - New test file to prevent future regressions (26 lines)

Final Verdict: ✅ APPROVED

While the original bug (missing _delivery.py) is already fixed on main, this PR still provides value by:

  • Improving code quality with the PERMANENT_ERROR_PREFIX constant
  • Making error detection more robust with startswith()
  • Adding regression tests to prevent the issue from recurring

The changes are minimal, focused, and improve maintainability. Ready to merge once CI passes.

--- |

…ivery

PR #2054 deleted bots/_delivery.py believing it was superseded by
bots/delivery.py, but delivery.py only provides DeliveryRouter and
ChannelDirectory. DurableDelivery, deliver_with_retry, and deliver_chunked
remain referenced by _durable_adapter.py and bots/__init__.py, causing
ModuleNotFoundError on any durable outbound delivery usage.

Restore the module and add regression tests for the public exports.

Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
@praisonai-triage-agent praisonai-triage-agent Bot force-pushed the cursor/critical-bug-investigation-3971 branch from 34e7ee0 to bc34800 Compare June 22, 2026 22:24
@MervinPraison MervinPraison merged commit 2eeb84f into main Jun 22, 2026
14 checks passed
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.

2 participants