Skip to content

fix: make chat approval backends durable across restart#3188

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

fix: make chat approval backends durable across restart#3188
praisonai-triage-agent[bot] wants to merge 2 commits into
mainfrom
claude/issue-3183-20260719-0912

Conversation

@praisonai-triage-agent

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

Copy link
Copy Markdown
Contributor

Fixes #3183

Summary

Chat approval backends (Slack, Telegram, Discord, Webhook, HTTP) held the pending human-in-the-loop approval only in an in-memory polling coroutine. If the gateway/bot restarted while an approval was outstanding, the pending request was lost and the blocked agent run was stranded.

This wires those backends to the durable ApprovalStore that already exists in-tree β€” kept intentionally minimal per the "lightweight and powerful" mandate (no new DurableChannelApproval base class, no new CLI/YAML knobs, no new dependency).

Changes

  • _approval_base.py: new opt-in DurableApprovalMixin (_persist_pending / _resolve_pending / rehydrate) that reuses the existing SQLite+WAL ApprovalStore; plus a single shared DEFAULT_APPROVAL_TIMEOUT (300s).
  • Slack / Telegram / Discord / Webhook / HTTP backends: now inherit the mixin and accept an optional store=. The request is persisted before polling and the final decision recorded; rehydrate() recovers outstanding approvals on startup.

Backward compatibility

Fully backward-compatible: when no store is supplied every durable method is a no-op and behaviour is exactly as before.

Test plan

  • New test_durable_channel_approval.py (6 tests): persist survives "restart", resolve closes the row, no-store no-op, end-to-end through HTTP backend.
  • Existing approval regression suite (125 tests) passes unchanged.

Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added durable approval tracking across Slack, Telegram, Discord, HTTP, and webhook channels.
    • Pending approvals can be restored after restarts, with final decisions recorded for auditability.
    • Added optional persistent storage while preserving existing behavior when storage is not configured.
    • Standardized the default approval timeout to five minutes.
  • Tests

    • Added coverage for persistence, recovery, resolution, timeout handling, and backward compatibility.

Chat approval backends (Slack, Telegram, Discord, Webhook, HTTP) held the
pending approval only in an in-memory polling coroutine, so a gateway/bot
restart while an approval was outstanding stranded the blocked agent run.

Add an opt-in DurableApprovalMixin to the shared _approval_base module that
wraps the existing ApprovalStore: it persists the request before polling,
records the final decision, and exposes rehydrate() to recover outstanding
approvals on startup. Each chat backend now accepts an optional store= and a
shared DEFAULT_APPROVAL_TIMEOUT. When no store is given behaviour is unchanged
(fully backward-compatible); no new dependency is introduced.

Co-authored-by: MervinPraison <MervinPraison@users.noreply.github.com>
@MervinPraison

Copy link
Copy Markdown
Owner

@coderabbitai review

@MervinPraison

Copy link
Copy Markdown
Owner

/review

@MervinPraison MervinPraison added pipeline/blocked:ci Blocked: CI not green on HEAD pipeline/blocked:no-final Blocked: no FINAL @claude trigger yet pipeline/final-claude-pending Reviews done; waiting for FINAL @claude labels Jul 19, 2026
@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: c1648560-5e27-4141-854e-31d7d14466bb

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

Chat approval backends now share a 300-second timeout constant and optional durable approval storage. Pending requests are persisted before polling, final decisions are recorded, and pending approvals can be rehydrated after restart. Unit tests cover lifecycle behavior, compatibility without a store, and HTTP timeout resolution.

Changes

Durable approval lifecycle

Layer / File(s) Summary
Shared durable approval lifecycle
src/praisonai-bot/praisonai_bot/bots/_approval_base.py
Adds DEFAULT_APPROVAL_TIMEOUT and DurableApprovalMixin for persistence, resolution, rehydration, expiry tracking, and guarded store failures.
Chat backend integration
src/praisonai-bot/praisonai_bot/bots/*_approval.py
Slack, Telegram, Discord, Webhook, and HTTP backends accept optional stores, initialize durable state, persist pending requests, and resolve final decisions.
Durability lifecycle validation
src/praisonai-bot/tests/unit/bots/test_durable_channel_approval.py
Tests backend inheritance, shared timeout configuration, restart rehydration, resolution cleanup, no-store behavior, and HTTP timeout persistence.

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

Sequence Diagram(s)

sequenceDiagram
  participant Agent
  participant ChatApprovalBackend
  participant DurableApprovalMixin
  participant ApprovalStore
  Agent->>ChatApprovalBackend: request approval
  ChatApprovalBackend->>DurableApprovalMixin: persist pending request
  DurableApprovalMixin->>ApprovalStore: save pending state
  ChatApprovalBackend-->>Agent: poll for approval decision
  ChatApprovalBackend->>DurableApprovalMixin: resolve decision
  DurableApprovalMixin->>ApprovalStore: record final decision
Loading

Suggested reviewers: mervinpraison

πŸš₯ Pre-merge checks | βœ… 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR adds durable persistence and rehydration, but the issue also calls for default actor authorization and typed approval actions, which aren't shown here. Add the missing actor-authorization and typed-action behavior across the chat backends, or narrow the issue scope to durability-only changes.
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
βœ… Passed checks (3 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: making chat approval backends durable across restarts.
Out of Scope Changes check βœ… Passed The changes stay focused on approval durability, shared timeout handling, and tests, with no clear unrelated additions.
✨ Finishing Touches
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/issue-3183-20260719-0912

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.

@greptile-apps

greptile-apps Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR wires the existing ApprovalStore (SQLite+WAL) into all five chat-channel approval backends (Slack, Telegram, Discord, Webhook, HTTP) via a new DurableApprovalMixin. When a store= is supplied, the pending approval is recorded before polling begins and the final decision closes the row; rehydrate() surfaces outstanding rows on startup. The change is fully backward-compatible β€” omitting store= leaves all methods as no-ops.

  • DurableApprovalMixin added to _approval_base.py: three async methods (_persist_pending, _resolve_pending, rehydrate) that delegate to the store and swallow all store exceptions to prevent persistence failures from breaking live approvals.
  • All five backends now inherit the mixin and cover every exit path (early-return guards, happy path, outer except) with a matching _resolve_pending call.
  • Eight new unit tests cover survive-restart, resolve-closes-row, no-store no-op, HTTP end-to-end timeout, and early-exit paths for Telegram and Discord.

Confidence Score: 5/5

Safe to merge β€” the mixin is fully opt-in and the no-store path is unchanged, so existing deployments are unaffected.

All five backends correctly call _resolve_pending on every exit path (early-return guards, happy path, and outer except handlers). The one finding β€” _build_blocks/_build_fallback_text being called after _persist_pending but before the Slack try/except β€” is an edge case that auto-expires within the configured timeout window and does not affect correctness for the normal approval flow.

_slack_approval.py: the two message-builder calls sit outside the try/except scope; moving them inside would close the last potential dangling-row path.

Important Files Changed

Filename Overview
src/praisonai-bot/praisonai_bot/bots/_approval_base.py Adds DurableApprovalMixin with _persist_pending/_resolve_pending/rehydrate no-op-safe methods, and the shared DEFAULT_APPROVAL_TIMEOUT constant.
src/praisonai-bot/praisonai_bot/bots/_slack_approval.py Wires DurableApprovalMixin; all explicit early-exit and exception paths now call _resolve_pending, but _build_blocks/_build_fallback_text are called after _persist_pending and outside the try/except, leaving a dangling row if they raise.
src/praisonai-bot/praisonai_bot/bots/_telegram_approval.py Wires DurableApprovalMixin; all exit paths (missing chat_id, failed send, happy path, outer except) correctly call _resolve_pending.
src/praisonai-bot/praisonai_bot/bots/_discord_approval.py Wires DurableApprovalMixin; all exit paths (missing channel_id, missing msg_id, happy path, outer except) correctly call _resolve_pending.
src/praisonai-bot/praisonai_bot/bots/_webhook_approval.py Wires DurableApprovalMixin; immediate-decision, poll-decision, and exception paths all correctly call _resolve_pending. No unguarded code between _persist_pending and the try block.
src/praisonai-bot/praisonai_bot/bots/_http_approval.py Wires DurableApprovalMixin; decided and timeout paths both call _resolve_pending correctly.
src/praisonai-bot/tests/unit/bots/test_durable_channel_approval.py 8 tests covering persist/rehydrate survival, resolve-closes-row, no-store no-op, HTTP end-to-end timeout, and early-exit paths for Telegram and Discord; exception-path coverage added.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Agent
    participant Backend as ChatBackend (Slack/Telegram/etc.)
    participant Mixin as DurableApprovalMixin
    participant Store as ApprovalStore (SQLite)
    participant Platform as Chat Platform

    Agent->>Backend: request_approval(request)
    Backend->>Mixin: _persist_pending(request, timeout)
    Mixin->>Store: persist(approval_id, request, expires_at)
    Store-->>Mixin: ok
    Backend->>Platform: POST message / open poll
    alt Happy path
        Platform-->>Backend: user replies approve/deny
        Backend->>Mixin: _resolve_pending(request, decision)
        Mixin->>Store: resolve(approval_id, decision)
    else Early exit (no channel / post failed)
        Backend->>Mixin: _resolve_pending(request, denial)
        Mixin->>Store: resolve(approval_id, denial)
    else Exception
        Backend->>Mixin: _resolve_pending(request, denial)
        Mixin->>Store: resolve(approval_id, denial)
    end
    Backend-->>Agent: ApprovalDecision

    note over Backend,Store: On restart
    Backend->>Mixin: rehydrate()
    Mixin->>Store: list_pending()
    Store-->>Mixin: [(approval_id, request), ...]
    Mixin-->>Backend: pending list for operator re-attachment
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 Agent
    participant Backend as ChatBackend (Slack/Telegram/etc.)
    participant Mixin as DurableApprovalMixin
    participant Store as ApprovalStore (SQLite)
    participant Platform as Chat Platform

    Agent->>Backend: request_approval(request)
    Backend->>Mixin: _persist_pending(request, timeout)
    Mixin->>Store: persist(approval_id, request, expires_at)
    Store-->>Mixin: ok
    Backend->>Platform: POST message / open poll
    alt Happy path
        Platform-->>Backend: user replies approve/deny
        Backend->>Mixin: _resolve_pending(request, decision)
        Mixin->>Store: resolve(approval_id, decision)
    else Early exit (no channel / post failed)
        Backend->>Mixin: _resolve_pending(request, denial)
        Mixin->>Store: resolve(approval_id, denial)
    else Exception
        Backend->>Mixin: _resolve_pending(request, denial)
        Mixin->>Store: resolve(approval_id, denial)
    end
    Backend-->>Agent: ApprovalDecision

    note over Backend,Store: On restart
    Backend->>Mixin: rehydrate()
    Mixin->>Store: list_pending()
    Store-->>Mixin: [(approval_id, request), ...]
    Mixin-->>Backend: pending list for operator re-attachment
Loading

Reviews (2): Last reviewed commit: "fix: resolve durable pending row on all ..." | Re-trigger Greptile

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

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/_discord_approval.py (1)

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

Dangling pending state on error and early-return paths.

The request_approval method persists the pending state early via _persist_pending, but constructs and returns a denied ApprovalDecision on failure paths without calling _resolve_pending. This leaves the request durably dangling as "pending" in the store, meaning a process restart will erroneously rehydrate a request that already failed.

Consider capturing the fallback decision in a variable and resolving it before returning, or using a finally block to ensure resolution.

  • src/praisonai-bot/praisonai_bot/bots/_discord_approval.py#L196-L201: Call _resolve_pending here, as well as on the early returns for missing channel_id (L157) and missing msg_id (L178).
  • src/praisonai-bot/praisonai_bot/bots/_slack_approval.py#L204-L209: Call _resolve_pending here, as well as on the early returns for missing channel (L166) and failed post (L183).
  • src/praisonai-bot/praisonai_bot/bots/_telegram_approval.py#L184-L189: Call _resolve_pending here, as well as on the early returns for missing chat_id (L143) and failed send (L164).
  • src/praisonai-bot/praisonai_bot/bots/_webhook_approval.py#L182-L187: Call _resolve_pending here before returning the error decision.
πŸ€– 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/_discord_approval.py` around lines 196 -
201, Ensure every failure and early-return path in request_approval resolves
persisted pending state before returning a denied decision. Update
_discord_approval.py:196-201 and its missing channel_id/msg_id paths,
_slack_approval.py:204-209 and its missing channel/failed post paths,
_telegram_approval.py:184-189 and its missing chat_id/failed send paths, and
_webhook_approval.py:182-187 to call _resolve_pending with the fallback decision
before returning; preserve successful approval flows.
πŸ€– 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.

Outside diff comments:
In `@src/praisonai-bot/praisonai_bot/bots/_discord_approval.py`:
- Around line 196-201: Ensure every failure and early-return path in
request_approval resolves persisted pending state before returning a denied
decision. Update _discord_approval.py:196-201 and its missing channel_id/msg_id
paths, _slack_approval.py:204-209 and its missing channel/failed post paths,
_telegram_approval.py:184-189 and its missing chat_id/failed send paths, and
_webhook_approval.py:182-187 to call _resolve_pending with the fallback decision
before returning; preserve successful approval flows.

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

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9ba95eae-b507-4f6f-ab89-4a527c3ae313

πŸ“₯ Commits

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

πŸ“’ Files selected for processing (7)
  • src/praisonai-bot/praisonai_bot/bots/_approval_base.py
  • src/praisonai-bot/praisonai_bot/bots/_discord_approval.py
  • src/praisonai-bot/praisonai_bot/bots/_http_approval.py
  • src/praisonai-bot/praisonai_bot/bots/_slack_approval.py
  • src/praisonai-bot/praisonai_bot/bots/_telegram_approval.py
  • src/praisonai-bot/praisonai_bot/bots/_webhook_approval.py
  • src/praisonai-bot/tests/unit/bots/test_durable_channel_approval.py

Comment thread src/praisonai-bot/tests/unit/bots/test_durable_channel_approval.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
  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 #3188 βœ… Approve (with fixes pushed)

Reviewed against AGENTS.md, validated all reviewer findings, and pushed fixes to this branch (a9a4a8b0b).

Phase 1 β€” Architecture

Check Result
Protocol-driven / routing βœ… Entirely in praisonai-bot wrapper; reuses existing ApprovalStore. No core-SDK bloat.
Backward compatible βœ… Fully opt-in β€” every durable method is a no-op when store=None.
Performance βœ… No hot-path impact; persistence only on the (already-slow) HITL wait path.
SDK value βœ… Genuine robustness win β€” outstanding approvals now survive a restart instead of stranding the blocked run.
Agent params βœ… Zero new Agent params.
New deps βœ… None.

Phase 2 β€” Valid issues fixed

Confirmed valid (CodeRabbit + Greptile, same root cause): backends called _persist_pending before polling but skipped _resolve_pending on early-return guards and outer except handlers, leaving stale status='pending' rows that rehydrate() surfaced as false-positive outstanding approvals after restart. HTTP was already correct.

Files modified:

  • _telegram_approval.py β€” resolve on no-chat_id, failed sendMessage, outer except
  • _discord_approval.py β€” resolve on no-channel_id, no-msg_id, outer except
  • _slack_approval.py β€” resolve on no-channel, failed post, outer except
  • _webhook_approval.py β€” resolve on outer except
  • test_durable_channel_approval.py β€” +3 regression tests (Telegram/Discord early-exit + exception path assert pending_count() == 0 & empty rehydrate())

Skipped: the CodeRabbit pre-merge "actor authorization / typed actions" scope note β€” those already exist in _approval_base.py (is_authorized_actor, normalize_approvers) and are out of scope for this durability-only PR. No changes to praisonai-rust / praisonai-ts per scope.

Phase 3 β€” Tests

  • tests/unit/bots/ -k "approval or durable": 44 passed
  • Full tests/unit/bots/: 608 passed, 1 pre-existing unrelated failure (test_critical_bug_fixes.py β†’ module 'praisonai' has no attribute 'tool_resolver', an env/setup issue independent of this PR).

Verdict: All blocking review findings resolved. LGTM β€” ready to merge. βœ…
Β· Branch

… paths

Slack, Telegram, Discord, and Webhook approval backends persisted the
pending approval before polling but skipped _resolve_pending on their
early-return guards (missing chat/channel id, failed send) and outer
except handlers. This left stale status='pending' rows that rehydrate()
surfaced as false-positive outstanding approvals after a restart.

Each missed exit path now records the fallback denial via _resolve_pending
before returning, matching the HTTP reference backend. Adds regression
tests for the early-exit and exception paths.

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.

Gateway approvals are lost on restart for chat channels β€” chat backends never use the durable ApprovalStore

1 participant