feat(telegram): Telegram versions of inbox-buddy, joke-bot, spotify-releases, hn-monitor#86
Conversation
…eleases, hn-monitor
Add Telegram-transport variants of the personal-use agents, each a separate
deployable persona reusing the base agent's transport-agnostic logic and swapping
the Slack transport for a shared Telegram transport.
- shared/telegram.ts: shared transport over @relayfile/relay-helpers
telegramClient() (the generic providerClient — no ergonomic client yet, see
relayfile-adapters#222). Message parsing, bareChatId (__title suffix strip),
conversation key, bot-loop/wrong-chat/empty guards, and a send()/replyToMessage
wrapper that threads natively via reply_to_message_id. tsconfig: include shared/.
- inbox-buddy-telegram: reuses ../inbox-buddy/lib/{gmail,prompt,conversation};
Gmail Q&A over Telegram. Trigger telegram `message`; scope /telegram/chats/**.
- joke-bot-telegram: conversational jokes + daily joke-of-the-day; sandbox:false
(HTTP writeback, no box).
- spotify-releases-telegram: daily new-releases message to a Telegram chat.
- hn-monitor-telegram: threaded digest (header + reply_to_message_id) + Q&A over
recent posts; claims seen before posting, never throws once the header lands.
- tests/telegram-agents.test.mjs: transport + one+ path per agent (156 total green).
Each deploy needs only ONE chat surface. Telegram auth is a Nango bot-token
connection. Catalog cutover to recognize telegram is tracked in
relayfile-adapters#222 / workforce#249 (authoring works today via persona-kit's
provider index-signature fallback; personas compile).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR introduces a shared Telegram transport module ( ChangesTelegram Agent Variants
Sequence Diagram(s)sequenceDiagram
participant Cron
participant hn_agent as hn-monitor-telegram
participant HN_Algolia as HN Algolia API
participant LLM
participant Memory
participant Telegram
Cron->>hn_agent: scheduled tick
hn_agent->>HN_Algolia: fetchFrontPage() — up to 30 hits
HN_Algolia-->>hn_agent: stories[]
hn_agent->>Memory: loadSeen() → seen IDs
hn_agent->>Memory: saveSeen(seen ∪ fresh) — provisional claim
hn_agent->>LLM: summarize(fresh stories) with timeout
LLM-->>hn_agent: { header, body }
hn_agent->>Telegram: send(header) → receipt with messageId
hn_agent->>Telegram: send(body, reply_to=messageId)
hn_agent->>Memory: savePost(digest record)
Note over hn_agent,Memory: Error before header → release claim (saveSeen old IDs)
Note over hn_agent,Memory: Error after header → keep claim (avoid duplicate header)
participant User
User->>hn_agent: telegram.message (question)
hn_agent->>Memory: loadPosts() → recent digests
hn_agent->>LLM: answer constrained to digest context
LLM-->>hn_agent: answer text
hn_agent->>Telegram: replyToMessage(user msg, answer)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces Telegram-specific sibling variants for several existing agents (hn-monitor, inbox-buddy, joke-bot, and spotify-releases), utilizing a new shared Telegram transport utility (shared/telegram.ts) for message parsing, native threading, and sending. Feedback on these changes highlights a few critical improvements: in joke-bot-telegram, the conversation history should be reversed to match the prompt's chronological order, and the LLM call should be wrapped in a try-catch block to prevent hangs or crashes. For spotify-releases-telegram, the date comparison should be adjusted to >= with a 'seen' ID tracker to avoid missing same-day releases, and the rendered release list should be capped to prevent exceeding Telegram's 4096-character message limit.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| async function recall(ctx: WorkforceCtx, tag: string): Promise<string[]> { | ||
| return toLines( | ||
| await ctx.memory | ||
| .recall('recent joke-bot conversation', { tags: [tag], limit: CONVO_TURNS, scope: 'workspace' }) | ||
| .catch(() => []) | ||
| ); | ||
| } |
There was a problem hiding this comment.
ctx.memory.recall returns items in reverse chronological order (newest first). Since toLines preserves this order, the history array passed to buildPrompt will have the newest messages first. However, the prompt text explicitly tells the LLM: "Conversation so far (oldest first):\n...". This contradiction can confuse the LLM and make it follow the conversation backwards. We should reverse the array to ensure it is in chronological order (oldest first).
| async function recall(ctx: WorkforceCtx, tag: string): Promise<string[]> { | |
| return toLines( | |
| await ctx.memory | |
| .recall('recent joke-bot conversation', { tags: [tag], limit: CONVO_TURNS, scope: 'workspace' }) | |
| .catch(() => []) | |
| ); | |
| } | |
| async function recall(ctx: WorkforceCtx, tag: string): Promise<string[]> { | |
| const lines = toLines( | |
| await ctx.memory | |
| .recall('recent joke-bot conversation', { tags: [tag], limit: CONVO_TURNS, scope: 'workspace' }) | |
| .catch(() => []) | |
| ); | |
| return lines.reverse(); | |
| } |
| const releases = perArtist | ||
| .flatMap((r) => (r.status === 'fulfilled' ? r.value : [])) | ||
| .filter((rel) => rel.date > since); |
There was a problem hiding this comment.
The filter rel.date > since uses strict inequality on YYYY-MM-DD date strings. If a release is published on the same day as the check (e.g., June 10) but after the daily cron job runs, its date will be "2026-06-10". On the next day's run (June 11), since will be "2026-06-10". The filter rel.date > since (i.e., "2026-06-10" > "2026-06-10") will evaluate to false, causing that release to be permanently missed. To prevent this, you should use >= comparison and track already-notified release IDs/URLs in memory (similar to how hn-monitor uses seen IDs) to filter out duplicates.
| const reply = await joke(ctx, buildPrompt(await recall(ctx, tag), question), deps.complete); | ||
| await replyToMessage(ctx, tg, msg, reply); | ||
| await remember(ctx, tag, question, reply); |
There was a problem hiding this comment.
The LLM call joke(...) is executed without any timeout or try-catch block. If the LLM call hangs or fails (due to rate limits, API issues, etc.), the agent will either hang indefinitely or crash, leaving the user with no reply. It is highly recommended to wrap the LLM call in a try-catch block to provide a friendly fallback response so the bot always replies.
| const reply = await joke(ctx, buildPrompt(await recall(ctx, tag), question), deps.complete); | |
| await replyToMessage(ctx, tg, msg, reply); | |
| await remember(ctx, tag, question, reply); | |
| let reply: string; | |
| try { | |
| reply = await joke(ctx, buildPrompt(await recall(ctx, tag), question), deps.complete); | |
| } catch (error) { | |
| ctx.log?.('warn', 'joke-bot-telegram.llm-failed', { error: String(error) }); | |
| reply = "I'm blanking on a punchline right now... must be writer's block! Try asking me again in a second."; | |
| } | |
| await replyToMessage(ctx, tg, msg, reply); | |
| await remember(ctx, tag, question, reply); |
| function render(releases: Release[]): string { | ||
| const lines = releases | ||
| .sort((a, b) => b.date.localeCompare(a.date)) | ||
| .map((r) => `• ${r.artist} — ${r.name} (${r.date})\n${r.url}`); | ||
| return `🎵 New releases from artists you follow (${releases.length})\n${lines.join('\n')}`; | ||
| } |
There was a problem hiding this comment.
Telegram has a strict maximum message length limit of 4096 characters. If a user follows many artists (up to 50 are fetched) and there are many new releases (e.g., on a Friday release day), the rendered list can easily exceed 4096 characters. When this happens, the Telegram API will reject the message with a 400 Bad Request error, and the notification will fail completely. To prevent this, you should slice the sorted releases to a safe maximum limit (e.g., 30) and append a summary of the remaining count.
| function render(releases: Release[]): string { | |
| const lines = releases | |
| .sort((a, b) => b.date.localeCompare(a.date)) | |
| .map((r) => `• ${r.artist} — ${r.name} (${r.date})\n${r.url}`); | |
| return `🎵 New releases from artists you follow (${releases.length})\n${lines.join('\n')}`; | |
| } | |
| function render(releases: Release[]): string { | |
| const sorted = [...releases].sort((a, b) => b.date.localeCompare(a.date)); | |
| const limit = 30; | |
| const shown = sorted.slice(0, limit); | |
| const lines = shown.map((r) => `• ${r.artist} — ${r.name} (${r.date})\n${r.url}`); | |
| let body = `🎵 New releases from artists you follow (${releases.length})\n${lines.join('\n')}`; | |
| if (releases.length > limit) { | |
| body += `\n\n...and ${releases.length - limit} more releases.`; | |
| } | |
| return body; | |
| } |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 91473aff7b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (releases.length > 0) { | ||
| const tg = deps.telegram ?? defaultTelegram(); | ||
| const res = await tg.send(bareChatId(chat), render(releases)); | ||
| if (!res.ok) ctx.log?.('warn', 'spotify-releases-telegram.no-receipt', { chat: bareChatId(chat), releases: releases.length }); |
There was a problem hiding this comment.
Do not checkpoint releases after failed sends
When Telegram writeback returns ok:false (the shared transport treats this as a missing receipt/silent drop), this branch only logs and then execution continues to saveLastCheck. In a writeback timeout or silent drop, the releases were never delivered but the next runs filter them out with rel.date > since, so those notifications are permanently lost instead of retried.
Useful? React with 👍 / 👎.
| const [item] = await ctx.memory.recall('spotify last check', { tags: ['spotify:last-check'], limit: 1 }); | ||
| return item?.content ?? '0000-00-00'; | ||
| } | ||
| async function saveLastCheck(ctx: WorkforceCtx, date: string): Promise<void> { | ||
| await ctx.memory.save(date, { tags: ['spotify:last-check'], scope: 'workspace' }); |
There was a problem hiding this comment.
Namespace the Telegram Spotify checkpoint
This reuses the same workspace memory tag as the existing Slack spotify-releases agent, so deploying the new Telegram sibling alongside Slack as advertised makes the two transports suppress each other: whichever cron saves spotify:last-check first causes the other to treat the same releases as already old. Use a Telegram-specific tag (or include the surface/chat) for both load and save so each deployable maintains its own delivery checkpoint.
Useful? React with 👍 / 👎.
Review: PR #86 — Telegram siblings of inbox-buddy, joke-bot, spotify-releases, hn-monitorSummaryAdditive feature PR. Creates 4 new I traced every reuse: Verification
Note on the full
|
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
tests/telegram-agents.test.mjs (1)
219-250: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winAdd a regression test for “header posted, body send fails”
Current tests cover header no-receipt rollback, but not the Line 171-181 branch where header is posted and body fails. Add a focused test for that path so this reliability edge case stays guarded during future changes.
🤖 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/telegram-agents.test.mjs` around lines 219 - 250, The test suite is missing coverage for the edge case where the header is successfully posted but the body send fails (Line 171-181 branch). Add a new test function similar to the existing "hn-monitor-telegram: a no-receipt header releases the claim and throws" test that mocks a telegram object where the first send call returns ok: true (header success) but the second send call returns ok: false (body failure). This test should verify that the system properly handles and rolls back from this specific failure path to ensure claim release and appropriate error propagation when body delivery fails after header posting succeeds.
🤖 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 `@hn-monitor-telegram/agent.ts`:
- Around line 171-181: The catch block (lines 171-181) suppresses retries after
the header is posted to prevent duplicate headers on retry, but this causes
permanent digest loss if the body send fails since there's no mechanism to retry
the body post later. Add a retryable "pending thread body" state or idempotency
key mechanism that tracks when a thread header has been posted but the body is
still pending, so that on the next execution cycle the body can be retried and
completed without reposting the header. Store this pending state alongside the
seen claim tracking so it persists across retries.
- Around line 186-199: The fetchFrontPage function makes an external API call to
the Hacker News Algolia endpoint without any timeout mechanism, which can cause
the cron job to hang indefinitely if the upstream service stalls. Add an
AbortController with a reasonable timeout duration (such as 5-10 seconds) to the
fetch call, and pass the abort signal to the fetch request. This ensures that if
the API request takes too long, it will be cancelled automatically and the catch
block will handle the error gracefully, preventing hung cron runs.
In `@spotify-releases-telegram/agent.ts`:
- Around line 44-58: The issue is that saveLastCheck is called unconditionally
regardless of whether any artist fetches failed, which advances the checkpoint
even when some releases couldn't be retrieved. This causes permanently missed
releases for failed artists. Check the perArtist array for any rejected promises
before calling saveLastCheck, and only advance the checkpoint if all artist
fetches succeeded by verifying that no results have a status of 'rejected'. This
ensures the checkpoint only moves forward when all data has been successfully
processed.
- Around line 79-83: The spotify() function makes unbounded fetch requests that
can hang indefinitely if the upstream service becomes unresponsive. Add a
timeout mechanism to the fetch call by using AbortController to automatically
abort the request after a reasonable duration (for example, 30 seconds). Create
an AbortController instance, set a timeout using setTimeout that calls
controller.abort() after the desired duration, and pass the controller.signal to
the fetch options. Also handle the resulting AbortError in the error handling
logic to provide a clear timeout-specific error message.
---
Nitpick comments:
In `@tests/telegram-agents.test.mjs`:
- Around line 219-250: The test suite is missing coverage for the edge case
where the header is successfully posted but the body send fails (Line 171-181
branch). Add a new test function similar to the existing "hn-monitor-telegram: a
no-receipt header releases the claim and throws" test that mocks a telegram
object where the first send call returns ok: true (header success) but the
second send call returns ok: false (body failure). This test should verify that
the system properly handles and rolls back from this specific failure path to
ensure claim release and appropriate error propagation when body delivery fails
after header posting succeeds.
🪄 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: CHILL
Plan: Pro Plus
Run ID: 56d0981e-cb2a-415f-80af-4cf16ce2e8e2
📒 Files selected for processing (15)
hn-monitor-telegram/README.mdhn-monitor-telegram/agent.tshn-monitor-telegram/persona.tsinbox-buddy-telegram/README.mdinbox-buddy-telegram/agent.tsinbox-buddy-telegram/persona.tsjoke-bot-telegram/README.mdjoke-bot-telegram/agent.tsjoke-bot-telegram/persona.tsshared/telegram.tsspotify-releases-telegram/README.mdspotify-releases-telegram/agent.tsspotify-releases-telegram/persona.tstests/telegram-agents.test.mjstsconfig.json
…-helpers 0.4.2)
relayfile-adapters shipped the ergonomic Telegram client (relay-helpers 0.4.2,
addressing the client half of relayfile-adapters#222). Bump the dep and use
telegramClient().sendMessage(chatId, text, { replyToMessageId, threadId })
instead of the low-level messages.write — it builds the body, applies writeback
idempotency, and returns { ok, messageId } directly, so the manual body
construction + receipt parsing in shared/telegram.ts is gone.
No agent/test changes: the injectable TelegramSender.send seam is unchanged, so
all four agents and the 156-test suite are untouched. The trigger/scope CATALOG
cutover (workforce#249) is still pending — adapter-core in node_modules has no
telegram catalog yet — so the persona index-signature-fallback note stays.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review of PR #86 — Telegram sibling agentsSummaryPR #86 adds four Telegram-transport sibling agents ( Verification (the way CI runs it)
Root cause of those 4 failures is environmental, not a PR defect: the harness sets FindingsNo code changes were warranted. Specific things checked and cleared:
Addressed comments
Advisory Notes
The PR itself is sound, type-clean, and fully covered by its own passing tests. The only red checks are pre-existing failures outside this PR's scope, so this is not ready to hand back as "all green for a human merge." |
Adds Telegram-transport siblings of the personal-use agents, so you can run them over Telegram instead of (or alongside) Slack. Each is a separate deployable persona that reuses its base agent's transport-agnostic logic and swaps the Slack transport for a shared Telegram transport — every deploy needs only ONE chat surface.
What's here
shared/telegram.ts— shared transport over@relayfile/relay-helperstelegramClient()(the genericproviderClient; there's no ergonomic telegram client yet — see relayfile-adapters#222). Provides message parsing,bareChatId(__titlesuffix strip), conversation keying, bot-loop / wrong-chat / empty guards, and asend()/replyToMessage()wrapper that threads natively viareply_to_message_id. (tsconfignow includesshared/.)inbox-buddy-telegram— reuses../inbox-buddy/lib/{gmail,prompt,conversation}; Gmail Q&A over Telegram. Triggertelegram: message; scope/telegram/chats/**.joke-bot-telegram— conversational jokes + daily joke-of-the-day;sandbox:false(HTTP writeback, no Daytona box).spotify-releases-telegram— daily new-releases message to a Telegram chat.hn-monitor-telegram— threaded digest (count header +reply_to_message_id) + Q&A over recent posts; claims "seen" before posting and never throws once the header lands (no duplicate header on retry).tests/telegram-agents.test.mjs— transport + one-or-more path per agent. Full suite 156/156 green, typecheck clean, all four personas compile.Auth / deploy
Telegram auth is a Nango bot-token connection (
@BotFather). The trigger/scope catalog cutover to formally recognizetelegramis tracked in relayfile-adapters#222 and workforce#249 — authoring works today via persona-kit's provider index-signature fallback, and personas compile; the deploy target must have the telegram adapter registered.Approach (separate Telegram persona per agent, reusing the transport-agnostic lib) was chosen with the requester.
🤖 Generated with Claude Code
Summary by cubic
Adds Telegram-transport siblings for inbox-buddy, joke-bot, spotify-releases, and hn-monitor using a shared Telegram transport with native threading. Now uses
telegramClient().sendMessage()from@relayfile/relay-helpers@0.4.2for simple, idempotent writeback; added READMEs and graphics for each persona.New Features
shared/telegram.ts: wrapper overtelegramClient().sendMessage()with parsing,bareChatId, conversation keys, loop/wrong-chat/empty guards, andsend()/replyToMessage()(threads viareply_to_message_idand forumthreadId).inbox-buddy-telegram: Gmail Q&A; reuses../inbox-buddy/lib/{gmail,prompt,conversation}; triggertelegram: message; optionalTELEGRAM_CHAT.joke-bot-telegram: conversational jokes + daily cron; per-conversation memory;sandbox:false(HTTP writeback).spotify-releases-telegram: daily new releases toTELEGRAM_CHAT; requiresSPOTIFY_TOKEN; no model.hn-monitor-telegram: cron digest + Q&A; posts a count header then threads the digest; claims “seen” before posting; retries pending bodies without duplicating the header.tests/telegram-agents.test.mjscover transport and key flows;tsconfig.jsonincludesshared/*.ts.Migration
@BotFather). Ensure the deploy target registers the Telegram adapter (relayfile-adapters#222, workforce#249)./telegram/chats/**so the writeback path mounts.TELEGRAM_CHATfor delivery;SPOTIFY_TOKENrequired for spotify-releases.Written for commit e9be003. Summary will update on new commits.