Skip to content

feat(issue-163): unify Telegram into single channel with ephemeral setup#164

Open
JonasBaeumer wants to merge 4 commits into
mainfrom
feat/issue-163-ephemeral-telegram-setup
Open

feat(issue-163): unify Telegram into single channel with ephemeral setup#164
JonasBaeumer wants to merge 4 commits into
mainfrom
feat/issue-163-ephemeral-telegram-setup

Conversation

@JonasBaeumer

@JonasBaeumer JonasBaeumer commented May 7, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #163

The bot UX previously created a dual-channel feeling: setup/test pings mingled with live notifications, and the seeded demo user was auto-linked to a dev chat via TELEGRAM_TEST_CHAT_ID. A more recent change added TELEGRAM_TEST_CHANNEL_ID to route test traffic to a separate Telegram group — but the goal of #163 is the opposite: keep one chat per user and make setup messages disappear after onboarding completes.

This PR delivers:

  • Ephemeral setup messages. During signup the bot tracks every message_id (its own + the user's /start and email reply) on the Redis signup session. On successful signup the bot sends the API-key message, then bulk-deletes the entire signup conversation and lands the user on the persistent main menu. Same cleanup runs when a stale session is replaced by a new /start. A TELEGRAM_SETUP_CLEANED audit event records the deleted count for observability after the chat is wiped.
  • Single onboarding path. TELEGRAM_TEST_CHAT_ID is removed from env.ts / seed.ts (demo user is no longer auto-linked); TELEGRAM_TEST_CHANNEL_ID is removed entirely. Live integration tests still read process.env.TELEGRAM_TEST_CHAT_ID as a local-dev escape hatch and gracefully skip when unset.
  • Live test fixes (surfaced while verifying the change):
    • waitForCallback filter compared cb.from.id against the configured chat ID — wrong in a group chat, where from.id is the tapping user's personal ID. Now compares cb.message.chat.id. Same fix in menuHandler.live.
    • Wrong-button alert no longer leaks raw callback_data identifiers; points to the instruction message instead.
  • deleteMessage added to mockBot. New unit and e2e tests cover messageIds tracking, success-path cleanup, stale-session cleanup, audit event, and best-effort deletion. docs/telegram-setup.md rewritten around a single canonical /start <code> path with an ephemeral-messages note up top.

Type of change

  • New feature
  • Refactor / cleanup
  • Tests only
  • Docs / config

Module(s) affected

  • Contracts (src/contracts/)
  • DB / Prisma (prisma/, src/db/)
  • Telegram (src/telegram/)
  • Tests / QA
  • Docs / Tooling

Checklist

  • npm test passes locally (376 unit tests; 2 pre-existing module-load failures on main unaffected)
  • New or changed logic has unit tests (tests/unit/telegram/signupHandler.test.ts — 5 new cases)
  • Integration tests added/updated (tests/integration/e2e/onboarding.test.ts — asserts cleanup + audit event)
  • No PAN, CVC, or card expiry is logged or stored
  • No new cross-module file edits
  • Types added/updated in src/contracts/ (AgentAuditEvent gains TELEGRAM_SETUP_CLEANED)
  • .env.example updated (removed TELEGRAM_TEST_CHAT_ID)

How to test

  1. docker compose up -d (Postgres + Redis)
  2. npm run db:migrate && npm run seed (creates demo@agentpay.dev, no chat auto-link)
  3. npm run dev, register webhook per docs/telegram-setup.md
  4. POST /v1/agent/register → grab pairing code → /start <code> in Telegram → Confirm → reply email
  5. Observe: all setup messages (incl. the API-key one) disappear after signup; only the main menu remains
  6. Trigger an approval flow (POST /v1/intents + npm run worker) — approval request stays (NOT deleted, that's intentional)
  7. Live test verification (already done):
    • npx jest --config jest.integration.live.js --testPathPattern=connectionFlow --forceExit → 5/5 pass
    • npx jest --config jest.integration.live.js --testPathPattern=preferences.live --forceExit --testTimeout=180000 → 12/12 pass interactively

Notes for reviewer

  • Removing TELEGRAM_TEST_CHANNEL_ID is intentional and reverses recent work; per the discussion on Remove demo mode dual-channel Telegram setup; unify into single bot channel #163 the dual-channel split was the UX problem we're solving, not a feature to keep.
  • connectionFlow.test.ts step 3.1 still skips gracefully if TELEGRAM_TEST_CHAT_ID is unset — no CI impact.
  • Nothing on the live-test files changes their non-live (mocked) behaviour.

Summary by CodeRabbit

  • New Features

    • Signup messages are sent ephemerally, tracked, bulk-deleted on success, and emit a TELEGRAM_SETUP_CLEANED audit event.
  • Documentation

    • Telegram setup unified to a single onboarding flow using /start ; removed optional test chat/channel env entries.
  • Chores

    • Example env and demo seed no longer include Telegram test chat/channel IDs.
  • Tests

    • Mocks and integration/unit tests updated to cover message tracking, deleteMessage, and cleanup assertions.

Review Change Stack

The bot UX previously created a dual-channel feeling: setup/test pings
mingled with live notifications, and the seeded demo user was
auto-linked to a dev chat. The recently added TELEGRAM_TEST_CHANNEL_ID
addressed this by routing test traffic to a separate group, but the
goal of #163 is the opposite — keep one chat per user and make setup
messages disappear after onboarding completes.

Behaviour changes:
- Track every bot-sent and user-sent message_id in the signup session.
- On successful signup, after the API-key message, bulk-delete all
  tracked messages and land the user on the persistent main menu.
- Same cleanup runs when a stale session is replaced by a new /start.
- Emit TELEGRAM_SETUP_CLEANED audit event with the deleted count.

Other changes:
- Drop TELEGRAM_TEST_CHAT_ID from env.ts/seed.ts; demo user is no
  longer auto-linked. Drop TELEGRAM_TEST_CHANNEL_ID and its docs.
- Live tests still read process.env.TELEGRAM_TEST_CHAT_ID as a
  local-dev escape hatch (skips when unset).
- Fix live-test bug: callback_query filter compared from.id (user)
  against the configured chat ID, dropping every tap in a group chat.
  Now compares message.chat.id. Same fix in menuHandler.live.
- Wrong-button alert in live tests no longer leaks raw callback_data.
- Add deleteMessage to mockBot; tests for cleanup, stale-session
  cleanup, and best-effort deletion.
- docs/telegram-setup.md rewritten around a single onboarding path.
@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Consolidates Telegram onboarding into a single /start pairing flow, tracks ephemeral signup message IDs in Redis, bulk-deletes them on completion, removes dual test env variables, updates seed/docs, extends mock Bot API (deleteMessage), and adapts unit/integration/e2e tests to assert cleanup and audit emission.

Changes

Telegram Single-Channel Ephemeral Setup

Layer / File(s) Summary
Data Contracts & Mock Support
src/contracts/audit.ts, src/telegram/mockBot.ts, src/config/env.ts
Adds TELEGRAM_SETUP_CLEANED audit literal; mock bot gains deleteMessage(chatId, messageId); removes TELEGRAM_TEST_* env exports/placeholders.
Session Store
src/telegram/sessionStore.ts
Adds per-chat message-id list helpers (appendSignupMessageId, getSignupMessageIds), TTL refresh, and clears both session key and message list on session clear.
Core Ephemeral Message Tracking
src/telegram/signupHandler.ts
Implements sendEphemeral that appends message IDs, deletes stale-session tracked messages on repeated /start, tracks incoming message IDs during signup, bulk-deletes tracked messages on success, emits TELEGRAM_SETUP_CLEANED with deleted count, and sends API key untracked.
Configuration / Seed / Docs
.env.example, src/db/seed.ts, docs/telegram-setup.md, CLAUDE.md, README.md
Removes TELEGRAM_TEST_CHAT_ID/TELEGRAM_TEST_CHANNEL_ID placeholders and docs; seed creates demo user without telegramChatId and logs { userId, email }; docs reworked to single /start <code> flow and troubleshooting updated.
Unit Tests & Mocks
tests/unit/*
Mocks updated: sendMessage returns incrementing message_id, deleteMessage mock added; Map-backed session-store in tests; added tests for message tracking, stale cleanup, bulk-delete best-effort retry, TELEGRAM_SETUP_CLEANED emission, and expanded callbackHandler unit coverage (approve/reject/idempotency/compensation flows).
Integration & E2E Tests
tests/integration/e2e/onboarding.test.ts, tests/integration/telegram/*.test.ts
E2E onboarding asserts message deletion ordering and TELEGRAM_SETUP_CLEANED audit event; live tests source chat ID from TELEGRAM_TEST_CHAT_ID only and filter by cb.message.chat.id/msg.chat.id; gating logic and teardown hardened for mock vs real mode.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • georgyia

Poem

🐰 The Telegram Hop
One channel now, so clean and bright,
Ephemeral whispers vanish out of sight.
I track each hop, then sweep them away,
Leaving only the messages meant to stay. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: unifying Telegram into a single channel with ephemeral setup messages, matching the PR's core objectives.
Linked Issues check ✅ Passed All primary coding requirements from issue #163 are met: dual-channel code removed, single bot channel for live/setup, ephemeral cleanup implemented, E2E tests updated, and docs rewritten.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #163 objectives; no unrelated modifications to non-Telegram channels or multi-user support were introduced.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-163-ephemeral-telegram-setup
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/issue-163-ephemeral-telegram-setup

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

@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: 2

🤖 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 `@tests/integration/e2e/telegramApprovalCheckout.test.ts`:
- Around line 44-47: Prettier formatting violations are causing CI failure in
this test file; run Prettier to reformat the file or manually fix formatting
around the hasTelegram const declaration and the code block at lines referenced
(the boolean expression combining isMockMode, process.env.TELEGRAM_BOT_TOKEN,
and process.env.TELEGRAM_TEST_CHAT_ID, and the subsequent block at 64-67) so the
file matches the project's Prettier rules—easiest fix: run prettier --write
"tests/integration/e2e/telegramApprovalCheckout.test.ts" and commit the
resulting changes.

In `@tests/integration/telegram/connectionFlow.test.ts`:
- Line 24: Prettier formatting in the Telegram integration test is failing;
reformat the test file that declares TELEGRAM_TEST_CHAT_ID and the later
connection-flow assertion block using Prettier (run your project's prettier
--write on that test file) so the constant declaration and the connection
assertions are properly formatted and CI passes.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 63fb4892-d76e-4b3a-8ff1-2737a5a410c2

📥 Commits

Reviewing files that changed from the base of the PR and between ee75883 and e5845b2.

📒 Files selected for processing (20)
  • .env.example
  • CLAUDE.md
  • README.md
  • docs/telegram-setup.md
  • src/config/env.ts
  • src/contracts/audit.ts
  • src/db/seed.ts
  • src/telegram/mockBot.ts
  • src/telegram/sessionStore.ts
  • src/telegram/signupHandler.ts
  • tests/integration/e2e/onboarding.test.ts
  • tests/integration/e2e/telegramApprovalCheckout.test.ts
  • tests/integration/telegram/connectionFlow.test.ts
  • tests/integration/telegram/menuHandler.live.test.ts
  • tests/integration/telegram/preferences.live.test.ts
  • tests/unit/api/rateLimit.test.ts
  • tests/unit/api/telegram.test.ts
  • tests/unit/telegram/callbackHandler.test.ts
  • tests/unit/telegram/notificationService.test.ts
  • tests/unit/telegram/signupHandler.test.ts
💤 Files with no reviewable changes (7)
  • README.md
  • tests/unit/api/rateLimit.test.ts
  • tests/unit/telegram/notificationService.test.ts
  • src/config/env.ts
  • tests/unit/api/telegram.test.ts
  • .env.example
  • tests/unit/telegram/callbackHandler.test.ts

Comment thread tests/integration/e2e/telegramApprovalCheckout.test.ts
Comment thread tests/integration/telegram/connectionFlow.test.ts

@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: 3

Caution

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

⚠️ Outside diff range comments (2)
tests/integration/e2e/telegramApprovalCheckout.test.ts (1)

84-90: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Guard cleanup deletes when IDs are unset to prevent broad Prisma deletes.

At Line 84-90, intentId/userId can be undefined if setup fails early, and deleteMany({ where: { ...: undefined } }) is unsafe. Add runtime guards before building these where filters.

🔧 Suggested fix
   afterAll(async () => {
-    await prisma.virtualCard.deleteMany({ where: { intentId } });
-    await prisma.ledgerEntry.deleteMany({ where: { intentId } });
-    await prisma.pot.deleteMany({ where: { intentId } });
-    await prisma.approvalDecision.deleteMany({ where: { intentId } });
-    await prisma.auditEvent.deleteMany({ where: { intentId } });
-    await prisma.purchaseIntent.deleteMany({ where: { id: intentId } });
-    await prisma.user.deleteMany({ where: { id: userId } });
+    if (intentId) {
+      await prisma.virtualCard.deleteMany({ where: { intentId } });
+      await prisma.ledgerEntry.deleteMany({ where: { intentId } });
+      await prisma.pot.deleteMany({ where: { intentId } });
+      await prisma.approvalDecision.deleteMany({ where: { intentId } });
+      await prisma.auditEvent.deleteMany({ where: { intentId } });
+      await prisma.purchaseIntent.deleteMany({ where: { id: intentId } });
+    }
+    if (userId) {
+      await prisma.user.deleteMany({ where: { id: userId } });
+    }
   });

Based on learnings: In Prisma queries, do not pass undefined inside where clauses because it can trigger unintended full-table operations.

🤖 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 `@tests/integration/e2e/telegramApprovalCheckout.test.ts` around lines 84 - 90,
The cleanup calls call prisma.deleteMany with potentially undefined
intentId/userId (variables intentId and userId), which can produce unsafe broad
deletes; add runtime guards so each deleteMany is only executed when the
corresponding id is truthy (e.g., check intentId before calling
prisma.virtualCard.deleteMany, prisma.ledgerEntry.deleteMany,
prisma.pot.deleteMany, prisma.approvalDecision.deleteMany,
prisma.auditEvent.deleteMany, prisma.purchaseIntent.deleteMany and check userId
before prisma.user.deleteMany), skipping or logging if the id is not set to
ensure no where filter contains undefined.
src/telegram/signupHandler.ts (1)

269-275: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cleanup is skipped on the "code already claimed" abort path.

This branch clears the signup session before deleting the tracked setup message ids, so /start, the confirmation prompt, and the email reply remain in chat forever when the claim race is hit. Cleanup needs to run before clearSignupSession(chatId) here as well.

Suggested direction
   } catch (err: any) {
     if (err.name === 'PairingCodeAlreadyClaimedError') {
+      const finalSession = await getSignupSession(chatId);
+      await cleanupSignupMessages(chatId, finalSession);
       await clearSignupSession(chatId);
       await bot.api.sendMessage(
         chatId,
         '⚠️ This pairing code was already claimed by another account. Please ask your OpenClaw assistant for a new code.',
       );
🤖 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/telegram/signupHandler.ts` around lines 269 - 275, The
PairingCodeAlreadyClaimedError branch clears the signup session before removing
tracked setup messages, leaving /start, confirmation prompt, and email reply in
chat; change the order so the cleanup that removes tracked message ids runs
before clearSignupSession(chatId). In the catch block that checks err.name ===
'PairingCodeAlreadyClaimedError' (in signupHandler.ts), call the same cleanup
function used elsewhere to delete tracked setup message ids (e.g.,
deleteTrackedSetupMessageIds or equivalent) prior to awaiting
clearSignupSession(chatId), and preserve the existing bot.api.sendMessage
behavior.
🤖 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/telegram/signupHandler.ts`:
- Around line 194-196: Replace the regex-based check using isValidEmail(email)
with a Zod string email parse: import z from "zod" (or reuse an existing schema)
and validate via a zod schema like z.string().email().safeParse(email); in the
signup flow in signupHandler.ts where sendEphemeral(chatId, "...") is currently
called, branch on the safeParse result (if not success, send the same ephemeral
message and return). Ensure you reference the existing sendEphemeral and chatId
variables and remove the isValidEmail dependency for this branch so validation
conforms to the src/ Zod rule.
- Around line 42-49: appendSignupMessageId currently does a read/modify/write
via getSignupSession and setSignupSession which can race; change it to use an
atomic update on the session store (instead of getSignupSession +
setSignupSession) so the append to messageIds is done inside a single atomic
operation. Replace the body of appendSignupMessageId with a call to the
session-store's atomic update method (or implement a new
updateSignupSessionAtomic) that receives chatId and a transformer callback which
reads the existing session, ensures session/messageIds exist, returns a new
session with messageIds = [...existingMessageIds, messageId], and resolves the
promise; keep the function name appendSignupMessageId and operate only on the
messageIds field to avoid clobbering other session data.
- Around line 243-255: The API key message sent by sendEphemeral is being
included in finalSession and removed by cleanupSignupMessages, so users can't
copy result.rawKey; change the flow to avoid deleting that message immediately
by either (A) send the API key message after calling
clearSignupSession/getSignupSession so its id is not appended to finalSession,
or (B) modify cleanupSignupMessages to accept an exclude list and pass the API
key message id to preserve it; update the code paths around sendEphemeral,
getSignupSession, clearSignupSession, and cleanupSignupMessages (and references
to session.agentId/result.rawKey) accordingly so the key message remains until
explicit acknowledgement.

---

Outside diff comments:
In `@src/telegram/signupHandler.ts`:
- Around line 269-275: The PairingCodeAlreadyClaimedError branch clears the
signup session before removing tracked setup messages, leaving /start,
confirmation prompt, and email reply in chat; change the order so the cleanup
that removes tracked message ids runs before clearSignupSession(chatId). In the
catch block that checks err.name === 'PairingCodeAlreadyClaimedError' (in
signupHandler.ts), call the same cleanup function used elsewhere to delete
tracked setup message ids (e.g., deleteTrackedSetupMessageIds or equivalent)
prior to awaiting clearSignupSession(chatId), and preserve the existing
bot.api.sendMessage behavior.

In `@tests/integration/e2e/telegramApprovalCheckout.test.ts`:
- Around line 84-90: The cleanup calls call prisma.deleteMany with potentially
undefined intentId/userId (variables intentId and userId), which can produce
unsafe broad deletes; add runtime guards so each deleteMany is only executed
when the corresponding id is truthy (e.g., check intentId before calling
prisma.virtualCard.deleteMany, prisma.ledgerEntry.deleteMany,
prisma.pot.deleteMany, prisma.approvalDecision.deleteMany,
prisma.auditEvent.deleteMany, prisma.purchaseIntent.deleteMany and check userId
before prisma.user.deleteMany), skipping or logging if the id is not set to
ensure no where filter contains undefined.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f390b3fb-fa84-4b73-89a7-189dea3a7ac0

📥 Commits

Reviewing files that changed from the base of the PR and between e5845b2 and 890eaca.

📒 Files selected for processing (5)
  • src/telegram/signupHandler.ts
  • tests/integration/e2e/onboarding.test.ts
  • tests/integration/e2e/telegramApprovalCheckout.test.ts
  • tests/integration/telegram/connectionFlow.test.ts
  • tests/unit/telegram/signupHandler.test.ts

Comment thread src/telegram/signupHandler.ts Outdated
Comment thread src/telegram/signupHandler.ts Outdated
Comment thread src/telegram/signupHandler.ts Outdated
- sessionStore: split signup messageIds out of the JSON session into a
  Redis list with atomic RPUSH appends, eliminating the read/modify/write
  race in concurrent webhook handlers. clearSignupSession now drops both
  keys.
- signupHandler:
  - replace local isValidEmail regex with z.string().email() to match the
    project rule on Zod validation in src/.
  - send the API-key success message via bot.api.sendMessage (not tracked)
    so the cleanup pass cannot delete it before the user copies the key.
  - run cleanupSignupMessages before clearSignupSession in the
    PairingCodeAlreadyClaimedError branch so abandoned signup messages
    are removed when a claim race aborts the flow.
- telegramApprovalCheckout.test: guard intentId/userId truthiness in
  afterAll so a beforeAll failure cannot trigger unfiltered Prisma
  deleteMany operations.
- unit tests updated to mock the new sessionStore surface and assert
  message-id tracking via the dedicated list rather than via the session
  blob.

@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

🤖 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/telegram/sessionStore.ts`:
- Around line 36-45: appendSignupMessageId currently calls rpush and expire
separately which can leave the list without TTL if expire fails; change it to
use Redis pipeline/multi so rpush and expire are sent together atomically (e.g.,
getRedisClient().pipeline() or .multi()), push the RPUSH and EXPIRE commands
into the pipeline for key `${MSGS_KEY_PREFIX}${chatId}`, execute the pipeline
and await its result, and handle any pipeline errors (log/throw) instead of
calling rpush/expire separately.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 89ef16da-ba00-45e8-b8c0-4c5e30e7f6a4

📥 Commits

Reviewing files that changed from the base of the PR and between 890eaca and c2d7d72.

📒 Files selected for processing (4)
  • src/telegram/sessionStore.ts
  • src/telegram/signupHandler.ts
  • tests/integration/e2e/telegramApprovalCheckout.test.ts
  • tests/unit/telegram/signupHandler.test.ts

Comment thread src/telegram/sessionStore.ts
Avoids an orphaned list (RPUSH succeeds but EXPIRE fails on a network
blip) that would otherwise live forever without a TTL.

@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

🤖 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/telegram/sessionStore.ts`:
- Around line 43-46: The pipeline exec result from
getRedisClient().pipeline().rpush(...).expire(...).exec() must be checked for
per-command errors because ioredis returns [err,result] tuples that don't
reject; update the code in sessionStore (where MSGS_KEY_PREFIX and
getRedisClient are used) to capture the array returned by pipeline().exec(),
iterate the tuples for errors, and handle them (e.g., throw or log and bail) so
a failed RPUSH or EXPIRE doesn't silently succeed—ensure you map errors to the
correct command (rpush then expire) when reporting.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6ae817e8-5f3f-485a-93ab-e17ff499197c

📥 Commits

Reviewing files that changed from the base of the PR and between c2d7d72 and 09fc75e.

📒 Files selected for processing (1)
  • src/telegram/sessionStore.ts

Comment on lines +43 to +46
const redis = getRedisClient();
const key = `${MSGS_KEY_PREFIX}${chatId}`;
await redis.pipeline().rpush(key, String(messageId)).expire(key, ttlSeconds).exec();
}

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For ioredis v5.10.1, does pipeline().exec()reject on command-level errors, or does it resolve with[err, result] tuples that must be checked manually?

💡 Result:

In ioredis 5.10.x (including 5.10.1), pipeline.exec does not reject the Promise for per-command errors (e.g., a command that returns an error reply). Instead, it resolves and returns an array where each entry is a tuple in the form [err, result]; you must check each tuple/err manually. The docs show that the callback err is “always null” and that results contain “[err, result]” responses for the queued commands. [1][2] Practical implication: treat pipeline.exec as “batch execution succeeded at the transport/batch level”, and inspect every element of the results array to detect command-level failures. [1][2] Note: pipeline.exec can still reject for non-command-level failures (e.g., connection issues). The cited docs specifically cover command-level errors: “err is always null” and “Each response follows the format [err, result]”. [1][2]

Citations:


🏁 Script executed:

# First, let's look at the file mentioned in the review
cat -n src/telegram/sessionStore.ts | head -60

Repository: JonasBaeumer/AgentWallet

Length of output: 2585


🏁 Script executed:

# Search for other pipeline.exec() calls in the codebase to see if this is a widespread pattern
rg "pipeline\(\)" --type ts -A 2

Repository: JonasBaeumer/AgentWallet

Length of output: 245


🏁 Script executed:

# Check if the function needs TypeScript strict mode and type annotations
head -50 src/telegram/sessionStore.ts | tail -20

Repository: JonasBaeumer/AgentWallet

Length of output: 909


🏁 Script executed:

# Verify the error handling pattern - check if Promise<void> functions throw or handle errors elsewhere
rg "throw|catch" src/telegram/sessionStore.ts

Repository: JonasBaeumer/AgentWallet

Length of output: 50


Check for command-level errors in pipeline execution.

At line 45, pipeline().exec() is awaited but its tuple results are not checked. With ioredis 5.10.1, command-level failures are returned per entry in the results array as [err, result] tuples and do not cause rejection. Without checking these results, a failed RPUSH or EXPIRE command will silently skip message-ID tracking or TTL refresh, potentially leaving orphaned data.

Suggested fix
 export async function appendSignupMessageId(
   chatId: number | string,
   messageId: number,
   ttlSeconds = DEFAULT_TTL_SECONDS,
 ): Promise<void> {
   const redis = getRedisClient();
   const key = `${MSGS_KEY_PREFIX}${chatId}`;
-  await redis.pipeline().rpush(key, String(messageId)).expire(key, ttlSeconds).exec();
+  const results = await redis.pipeline().rpush(key, String(messageId)).expire(key, ttlSeconds).exec();
+  for (const [err] of results ?? []) {
+    if (err) throw err;
+  }
 }
🤖 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/telegram/sessionStore.ts` around lines 43 - 46, The pipeline exec result
from getRedisClient().pipeline().rpush(...).expire(...).exec() must be checked
for per-command errors because ioredis returns [err,result] tuples that don't
reject; update the code in sessionStore (where MSGS_KEY_PREFIX and
getRedisClient are used) to capture the array returned by pipeline().exec(),
iterate the tuples for errors, and handle them (e.g., throw or log and bail) so
a failed RPUSH or EXPIRE doesn't silently succeed—ensure you map errors to the
correct command (rpush then expire) when reporting.

@georgyia

Copy link
Copy Markdown
Collaborator

Status (2026-05-13): GitHub reports CONFLICTING / DIRTY vs main. Please rebase or merge main and resolve conflicts so CI can run on a clean merge base; then re-request review. (Related: #112 and #95 are in the same state.)

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.

Remove demo mode dual-channel Telegram setup; unify into single bot channel

2 participants