Skip to content

fix: exactly-once delivery reconciliation on Slack adapter#3187

Open
praisonai-triage-agent[bot] wants to merge 2 commits into
mainfrom
claude/issue-3185-20260719-0914
Open

fix: exactly-once delivery reconciliation on Slack adapter#3187
praisonai-triage-agent[bot] wants to merge 2 commits into
mainfrom
claude/issue-3185-20260719-0914

Conversation

@praisonai-triage-agent

@praisonai-triage-agent praisonai-triage-agent Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Fixes #3185

Summary

The outbox has a crash-safe, effectively-once delivery seam (recovered state + reconciler + PlatformCapabilities.reconciles_unknown_send), but no shipped adapter implemented it β€” so every built-in channel fell back to at-least-once and a gateway restart between platform-send and durable-ack re-delivered the message (a user-visible duplicate).

This wires the seam end-to-end for Slack, the one built-in channel whose platform API can genuinely confirm a client-side-keyed send (read back recent history + match message metadata). Channels whose platform cannot confirm delivery stay at-least-once by design (now an explicit per-channel fact, not a silent default).

Changes

  • praisonai_bot/bots/slack.py
    • SlackBot.default_capabilities() / platform_capabilities now declare reconciles_unknown_send=True.
    • send_message(..., idempotency_key=None) stamps the key into Slack message metadata (event_type="praisonai_outbound").
    • was_delivered(target, idempotency_key) queries conversations.history and matches the key in metadata.event_payload; best-effort (returns False β†’ at-least-once fallback on any lookup failure).
  • praisonai_bot/bots/_delivery.py
    • DurableDelivery carries idempotency_key in the outbox payload and forwards it to adapters whose send_message accepts it (signature-checked, backward-compatible β€” Telegram/Discord/etc. untouched).
    • _build_reconciler passes the target channel to was_delivered when its signature accepts it; the legacy single-arg was_delivered(idempotency_key) form still works.

Behaviour

Before: sending -> (restart) -> recovered -> re-dispatched blindly -> duplicate
After:  sending -> (restart) -> recovered -> reconciler confirms -> marked sent (no duplicate)

Layer placement

Wrapper-only (praisonai-bot adapters). The core PlatformCapabilities flag, reconciler contract, and outbox state machine already existed β€” nothing in the core contract changed. No new params/CLI/YAML surface; runtime adapter behaviour only.

Tests

New tests/unit/bots/test_slack_reconcile.py covers capability declaration, metadata stamping, was_delivered matching, and end-to-end drain reconcile (no re-send when already delivered; re-send when not). All 121 relevant delivery/outbox/slack tests pass; existing test_outbox_reconcile.py remains green.

Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved Slack message delivery reliability by preventing duplicate sends after interruptions.
    • Added delivery tracking metadata to support safe message reconciliation.
    • Automatically retries messages only when a previous delivery cannot be confirmed.
    • Preserves delivery tracking information during recovered or delayed sends.
  • Bug Fixes

    • Prevented duplicate Slack messages when a prior send succeeded but the process stopped before confirmation.

…ixes #3185)

The outbox's crash-reconciliation seam shipped unused: no channel adapter
implemented was_delivered() or declared reconciles_unknown_send, so every
built-in channel fell back to at-least-once (a duplicate on restart).

Slack can read back recent channel history and match a client-side idempotency
key stamped into the message metadata, so it now opts into effectively-once:
  - SlackBot declares reconciles_unknown_send=True and implements was_delivered()
    via conversations.history, matching the key in message metadata.
  - send_message stamps the idempotency key into Slack metadata when provided.
  - DurableDelivery carries the idempotency key in the outbox payload and
    forwards it to adapters whose send_message accepts it (backward-compatible).
  - _build_reconciler now passes the target channel to was_delivered when its
    signature accepts it (legacy single-arg form still supported).

Channels whose platform cannot confirm delivery stay at-least-once by design.

Co-authored-by: MervinPraison <MervinPraison@users.noreply.github.com>
@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 Jul 19, 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 Jul 19, 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: d6f9ee60-8e8b-45ef-935a-d1557b539c84

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

DurableDelivery now preserves idempotency keys across outbox recovery. Slack embeds keys in message metadata, declares reconciliation support, and checks recent history before resending. Tests cover metadata, matching, recovery without resend, and resend when delivery is absent.

Changes

Slack effectively-once delivery

Layer / File(s) Summary
Durable delivery key propagation
src/praisonai-bot/praisonai_bot/bots/_delivery.py
Outbox payloads retain idempotency keys, recovered sends forward them when supported, and reconcilers accept both current and legacy was_delivered signatures.
Slack idempotency adapter
src/praisonai-bot/praisonai_bot/bots/slack.py
Slack declares reconciliation capability, stamps outbound messages with idempotency metadata, and searches recent history for matching keys.
Recovery behavior validation
src/praisonai-bot/tests/unit/bots/test_slack_reconcile.py
Fake-client tests validate capability declaration, metadata stamping, history matching, recovery without resend, and resend with preserved metadata.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DurableDelivery
  participant SlackBot
  participant SlackAPI
  participant Outbox
  DurableDelivery->>Outbox: Load recovered entry and idempotency_key
  DurableDelivery->>SlackBot: Call was_delivered(target, idempotency_key)
  SlackBot->>SlackAPI: Query recent channel history with metadata
  SlackAPI-->>SlackBot: Return recent messages
  alt Matching metadata exists
    SlackBot-->>DurableDelivery: Return delivered
    DurableDelivery->>Outbox: Mark entry sent
  else No matching metadata
    DurableDelivery->>SlackBot: Resend with idempotency_key
    SlackBot->>SlackAPI: Post message with metadata
    DurableDelivery->>Outbox: Mark entry sent
  end
Loading
πŸš₯ 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 clearly summarizes the main change: Slack adapter delivery reconciliation for effectively-once handling.
Linked Issues check βœ… Passed The PR implements the linked crash-reconciliation seam in Slack with was_delivered and reconciles_unknown_send support.
Out of Scope Changes check βœ… Passed The code changes stay focused on Slack reconciliation, outbox forwarding, and targeted tests.
Docstring Coverage βœ… Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/issue-3185-20260719-0914

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.

@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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/praisonai-bot/praisonai_bot/bots/_delivery.py (1)

511-553: πŸ—„οΈ Data Integrity & Integration | πŸ”΄ Critical | ⚑ Quick win

Forward the outbox idempotency key on the initial send path
payload["idempotency_key"] is persisted, but this deliver_with_retry(...) call still sends only **send_kwargs. The first delivery never stamps the platform message with the key, so a crash after a successful send and before mark_sent() can’t be reconciled and will be resent as a duplicate. Mirror the drain_pending() sender logic here and pass the key when send_message accepts it.

πŸ€– 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-bot/praisonai_bot/bots/_delivery.py` around lines 511 - 553,
The initial delivery in the outbox path must forward the generated idempotency
key so sent messages can be reconciled after a crash. Update the
deliver_with_retry call in the enqueue/send flow to pass idempotency_key using
the same convention as drain_pending(), while preserving compatibility with
adapters whose send_message does not accept that argument.
πŸ€– 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.

Inline comments:
In `@src/praisonai-bot/praisonai_bot/bots/slack.py`:
- Around line 828-863: The was_delivered method must inspect thread replies when
a thread parent timestamp is available, since conversations_history excludes
threaded replies. Extract or accept the relevant thread_ts for the target, call
conversations.replies for that parent, and match
metadata.event_payload.idempotency_key using the existing logic; retain
conversations_history for non-threaded messages and return False on unavailable
lookups.

---

Outside diff comments:
In `@src/praisonai-bot/praisonai_bot/bots/_delivery.py`:
- Around line 511-553: The initial delivery in the outbox path must forward the
generated idempotency key so sent messages can be reconciled after a crash.
Update the deliver_with_retry call in the enqueue/send flow to pass
idempotency_key using the same convention as drain_pending(), while preserving
compatibility with adapters whose send_message does not accept that argument.
πŸͺ„ Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3dc3dabf-4b4e-4798-9b93-46791d517bc1

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 882143d and db121c4.

πŸ“’ Files selected for processing (3)
  • src/praisonai-bot/praisonai_bot/bots/_delivery.py
  • src/praisonai-bot/praisonai_bot/bots/slack.py
  • src/praisonai-bot/tests/unit/bots/test_slack_reconcile.py

Comment on lines +828 to +863

async def was_delivered(self, target: str, idempotency_key: str) -> bool:
"""Confirm whether a prior send for ``idempotency_key`` already landed.

Enables effectively-once delivery: after a crash between the Slack API
call and the durable ack, the outbox asks this before re-sending. We
read back recent channel history and match the client-side idempotency
key carried in each message's ``metadata.event_payload`` β€” so a
confirmed send is marked ``sent`` instead of producing a duplicate.

Returns ``False`` (fall back to at-least-once re-send) when the lookup
is unavailable or the key is not found.
"""
if not self._client or not idempotency_key:
return False

# ``target`` is the outbox target ("slack:<channel>"); strip the prefix.
channel_id = target.split(":", 1)[1] if ":" in target else target
try:
response = await self._client.conversations_history(
channel=channel_id,
limit=100,
include_all_metadata=True,
)
except Exception as e: # noqa: BLE001 β€” best-effort; never crash drain
logger.debug("was_delivered history lookup failed: %s", e)
return False

for message in response.get("messages", []) or []:
metadata = message.get("metadata") or {}
if metadata.get("event_type") != _OUTBOUND_EVENT_TYPE:
continue
payload = metadata.get("event_payload") or {}
if str(payload.get("idempotency_key", "")) == str(idempotency_key):
return True
return False

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.

πŸ—„οΈ Data Integrity & Integration | 🟠 Major | πŸ—οΈ Heavy lift

🧩 Analysis chain

🌐 Web query:

Does Slack's conversations.history Web API endpoint include thread replies, or only top-level channel messages?

πŸ’‘ Result:

No, Slack's conversations.history Web API endpoint does not include thread replies [1][2]. It only returns top-level messages within a channel [3][2]. To retrieve the replies associated with a thread, you must use the conversations.replies method, which requires the timestamp (ts) of the thread's parent message [4][3]. The conversations.history method will return the parent message if it exists within the requested time range, but it will not automatically include the subsequent threaded replies in its response [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="src/praisonai-bot/praisonai_bot/bots/slack.py"

echo "== relevant symbols =="
rg -n "def send_message|def was_delivered|thread_id|thread_ts|DurableDelivery|conversations_history|conversations_replies|_OUTBOUND_EVENT_TYPE" "$file"

echo
echo "== surrounding code around send_message and was_delivered =="
sed -n '760,890p' "$file"

Repository: MervinPraison/PraisonAI

Length of output: 8799


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== was_delivered call sites =="
rg -n "was_delivered\(" -S src/praisonai-bot

echo
echo "== DurableDelivery / deliver_outbound definition and call shape =="
rg -n "def deliver_outbound|class DurableDelivery|was_delivered|send_kwargs|send_message\(" src/praisonai-bot/praisonai_bot -S

Repository: MervinPraison/PraisonAI

Length of output: 13687


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the surrounding delivery integration without running project code.
file="src/praisonai-bot/praisonai_bot/bots/slack.py"
sed -n '1,220p' "$file"
echo
sed -n '220,360p' "$file"
echo
sed -n '360,520p' "$file"

Repository: MervinPraison/PraisonAI

Length of output: 21680


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where SlackBot.was_delivered is invoked and what context is available there.
rg -n "was_delivered\(" .

Repository: MervinPraison/PraisonAI

Length of output: 1630


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="src/praisonai-bot/praisonai_bot/bots/_delivery.py"

echo "== durable delivery reconcile path =="
sed -n '560,690p' "$file"

echo
echo "== relevant slack tests =="
sed -n '1,140p' "src/praisonai-bot/tests/unit/bots/test_slack_reconcile.py"

Repository: MervinPraison/PraisonAI

Length of output: 10374


Threaded sends need a reply-history lookup conversations_history only returns top-level channel messages, so a crash-recovered send with thread_ts will never match the stamped idempotency key and will be re-sent. Use conversations.replies when a thread parent ts is available, or threaded replies will silently lose effectively-once delivery.

πŸ€– 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-bot/praisonai_bot/bots/slack.py` around lines 828 - 863, The
was_delivered method must inspect thread replies when a thread parent timestamp
is available, since conversations_history excludes threaded replies. Extract or
accept the relevant thread_ts for the target, call conversations.replies for
that parent, and match metadata.event_payload.idempotency_key using the existing
logic; retain conversations_history for non-threaded messages and return False
on unavailable lookups.

@greptile-apps

greptile-apps Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR wires the pre-existing outbox exactly-once seam end-to-end for Slack by declaring reconciles_unknown_send=True, stamping a client-side idempotency key into Slack message metadata, and implementing was_delivered via conversations.history/conversations.replies. It also makes DurableDelivery pass idempotency_key and target/thread_id to adapters that declare the param, backward-compatibly.

  • slack.py: Adds default_capabilities (reconciles_unknown_send=True), stamps metadata.event_payload.idempotency_key on every keyed send, and adds was_delivered(target, key, thread_id?) that reads back recent channel history to confirm delivery. A scope-detection helper (_is_missing_metadata_scope) degrades to at-least-once when the metadata.message:write OAuth scope is missing.
  • _delivery.py: idempotency_key is now persisted inside the outbox payload and forwarded only to adapters whose send_message explicitly declares the param. _build_reconciler is extended to pass target and thread_id to was_delivered.
  • test_slack_reconcile.py: New unit tests covering capability declaration, metadata stamping, was_delivered key matching, and drain reconciliation for both re-send and no-re-send paths.

Confidence Score: 4/5

The core delivery logic is sound; the scope-detection helper _is_missing_metadata_scope has an overly broad match that should be tightened before merging.

The _is_missing_metadata_scope helper matches any error containing both 'metadata' and 'missing', which can silently strip the idempotency key from messages whose send failed for an unrelated reason. The fix is a one-liner but should land before merge.

slack.py lines 45-52 (_is_missing_metadata_scope); the delivery engine changes in _delivery.py are straightforward and low risk.

Important Files Changed

Filename Overview
src/praisonai-bot/praisonai_bot/bots/slack.py Adds was_delivered, send_message metadata stamping, and default_capabilities; _is_missing_metadata_scope is overly broad and can silently strip idempotency metadata on non-scope errors.
src/praisonai-bot/praisonai_bot/bots/_delivery.py Adds idempotency_key to the outbox payload and extends _build_reconciler; wants_thread keeps a VAR_KEYWORD fallback inconsistent with the rest of the PR.
src/praisonai-bot/tests/unit/bots/test_slack_reconcile.py New test module covering capability, metadata stamping, was_delivered matching, drain reconciliation, and missing-scope fallback.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant OB as OutboundQueue
    participant DD as DurableDelivery
    participant SB as SlackBot
    participant SA as Slack API

    Note over DD,SB: First send (happy path)
    DD->>OB: enqueue(idempotency_key, payload)
    DD->>SB: "send_message(..., idempotency_key=K)"
    SB->>SA: "chat.postMessage(metadata={key: K})"
    SA-->>SB: ts
    SB-->>DD: BotMessage
    DD->>OB: mark_sent(key)

    Note over DD,SB: Crash-recovery path (drain_pending)
    OB-->>DD: "recovered entries (status=sending)"
    DD->>DD: _build_reconciler()
    DD->>SB: was_delivered(target, K, thread_id?)
    SB->>SA: "conversations.history(include_all_metadata=True)"
    SA-->>SB: messages[]
    alt key found in metadata
        SB-->>DD: True
        DD->>OB: mark_sent(key)
        Note over DD: no duplicate send
    else key not found
        SB-->>DD: False
        DD->>SB: "send_message(..., idempotency_key=K)"
        SB->>SA: "chat.postMessage(metadata={key: K})"
        DD->>OB: mark_sent(key)
    end

    Note over SB,SA: Scope fallback
    SB->>SA: "chat.postMessage(metadata={key: K})"
    SA-->>SB: missing_scope error
    SB->>SA: chat.postMessage (no metadata)
    SA-->>SB: ts - at-least-once
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 OB as OutboundQueue
    participant DD as DurableDelivery
    participant SB as SlackBot
    participant SA as Slack API

    Note over DD,SB: First send (happy path)
    DD->>OB: enqueue(idempotency_key, payload)
    DD->>SB: "send_message(..., idempotency_key=K)"
    SB->>SA: "chat.postMessage(metadata={key: K})"
    SA-->>SB: ts
    SB-->>DD: BotMessage
    DD->>OB: mark_sent(key)

    Note over DD,SB: Crash-recovery path (drain_pending)
    OB-->>DD: "recovered entries (status=sending)"
    DD->>DD: _build_reconciler()
    DD->>SB: was_delivered(target, K, thread_id?)
    SB->>SA: "conversations.history(include_all_metadata=True)"
    SA-->>SB: messages[]
    alt key found in metadata
        SB-->>DD: True
        DD->>OB: mark_sent(key)
        Note over DD: no duplicate send
    else key not found
        SB-->>DD: False
        DD->>SB: "send_message(..., idempotency_key=K)"
        SB->>SA: "chat.postMessage(metadata={key: K})"
        DD->>OB: mark_sent(key)
    end

    Note over SB,SA: Scope fallback
    SB->>SA: "chat.postMessage(metadata={key: K})"
    SA-->>SB: missing_scope error
    SB->>SA: chat.postMessage (no metadata)
    SA-->>SB: ts - at-least-once
Loading

Reviews (2): Last reviewed commit: "fix: harden Slack effectively-once deliv..." | Re-trigger Greptile

Comment thread src/praisonai-bot/praisonai_bot/bots/slack.py
Comment thread src/praisonai-bot/praisonai_bot/bots/_delivery.py Outdated
@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
  4. SDK value: review in depth whether the change genuinely adds value to the SDK β€” never add features for the sake of adding them. It must strengthen the SDK (simpler, more user-friendly, robust, world-class, secure). If it does not clearly add value, request changes or recommend rejecting/closing rather than merging scope creep
  5. Do not bloat the Agent class with additional params β€” only if absolutely required; we already support many params.
  6. Repo routing: agent-callable tools β†’ PraisonAI-Tools; lifecycle plugins β†’ PraisonAI-Plugins; optional sandbox backends β†’ PraisonAI-Plugins (praisonai.sandbox entry point) β€” request changes if wrongly added to praisonaiagents/

Phase 2: FIX Valid Issues
7. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix
8. Also independently identify and fix any gaps or issues you find in the changed code β€” do not rely only on prior reviewer feedback
9. Push all code fixes directly to THIS branch (do NOT create a new PR)
10. Comment a summary of exact files modified and what you skipped

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

@MervinPraison MervinPraison added pipeline/awaiting-merge-gate FINAL done; waiting for merge gate / CI pipeline/blocked:cooldown Blocked: post-push or @claude cooldown and removed pipeline/final-claude-pending Reviews done; waiting for FINAL @claude pipeline/blocked:no-final Blocked: no FINAL @claude trigger yet labels Jul 19, 2026
@praisonai-triage-agent

praisonai-triage-agent Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

Claude finished @MervinPraison's task β€”β€” View job


Final Architecture Review β€” PR #3187 βœ…

  • Read AGENTS.md & changed files
  • Evaluate reviewer findings (CodeRabbit, Greptile)
  • Fix valid issues
  • Run tests (143 passed, 1 skipped)
  • Push & summarize

Phase 1 β€” Architecture (per AGENTS.md)

Criterion Verdict
Protocol-driven βœ… Wrapper-only (praisonai-bot). Core PlatformCapabilities / reconciler contract / outbox state machine already existed β€” no core contract changed.
Backward compatible βœ… New param is keyword-optional; other adapters (Telegram/Discord/etc.) untouched; legacy single-arg was_delivered still supported.
Performance βœ… No hot-path regression. Reconcile lookups run only on crash-recovered drain entries, not on the normal send path.
SDK value βœ… Genuine value: wires an existing-but-dead effectively-once seam end-to-end for the one channel that can confirm a keyed send. No scope creep, no new CLI/YAML/Agent params.
Agent bloat βœ… Zero changes to Agent.
Repo routing βœ… Correct β€” runtime adapter behaviour in praisonai-bot, not praisonaiagents/.

Phase 2 β€” Fixes pushed (commit 913122f)

bots/slack.py

  1. [Greptile P1 β€” CRITICAL] missing_scope regression fixed. Stamping metadata needs the metadata.message:write OAuth scope not granted by default. Previously a missing scope β†’ missing_scope β†’ permanent failure β†’ message lost (worse than the old at-least-once). Now send_message catches the scope error via _is_missing_metadata_scope() and retries without metadata, degrading gracefully to at-least-once.
  2. [CodeRabbit β€” Major] Threaded sends now reconciled. conversations.history excludes thread replies, so a threaded send's key was never found β†’ duplicate. was_delivered now accepts thread_id and queries conversations.replies for threaded targets.

bots/_delivery.py
3. [CodeRabbit β€” Critical] First-send key forwarding. The initial (non-drain) send now also stamps the idempotency key, so a crash between a successful send and mark_sent() can be reconciled instead of blindly re-sent.
4. [Greptile P2] Dropped the permissive **VAR_KEYWORD fallback in _send_accepts_idempotency_key() β€” now gated on an explicit idempotency_key param, so a generic **kwargs adapter won't receive an unexpected kwarg.
5. Reconciler now extracts thread_id from the outbox payload and forwards it to was_delivered (signature-checked, legacy forms preserved).

tests/unit/bots/test_slack_reconcile.py
6. Added test_send_falls_back_when_metadata_scope_missing and test_was_delivered_uses_replies_for_threaded_send; extended fake client with conversations_replies + a _MissingScopeClient.

Skipped (with reason): Qodo review paused (no findings). No other reviewer action items outstanding.

Phase 3 β€” Verdict

βœ… Approve. All valid blocking findings resolved. Tests: 143 passed, 1 skipped across test_slack_reconcile, test_outbox_reconcile, test_outbound_retry, test_outbound_resilience and related delivery suites (the one pre-existing pydantic-missing collection error in test_session_compaction.py is unrelated to this PR). Change is lightweight, backward-compatible, and correctly layered.

Note: I can't formally approve via GitHub's review API for security reasons β€” this comment is the review.

Job run β€’ branch claude/issue-3185-20260719-0914

…threads, first-send key)

Addresses reviewer findings on PR #3187:
- Graceful at-least-once fallback when metadata.message:write scope is absent
  (was a permanent message-loss regression on apps without the scope).
- Reconcile threaded sends via conversations.replies (history excludes replies).
- Forward idempotency_key on the initial send path, not just crash-recovered drain.
- Gate key forwarding on an explicit idempotency_key param (drop **kwargs fallback).

Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
@MervinPraison MervinPraison added pipeline/blocked:stale-final Blocked: FINAL stale after new commits and removed pipeline/blocked:stale-final Blocked: FINAL stale after new commits labels Jul 19, 2026
@praisonai-triage-agent praisonai-triage-agent Bot removed the pipeline/blocked:cooldown Blocked: post-push or @claude cooldown label Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pipeline/awaiting-merge-gate FINAL done; waiting for merge gate / CI pipeline/blocked:ci Blocked: CI not green on HEAD

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Built-in channels re-send on crash-after-dispatch: the exactly-once reconciliation seam (was_delivered) ships unused

1 participant