Skip to content

feat(telegram): Telegram versions of inbox-buddy, joke-bot, spotify-releases, hn-monitor#86

Merged
khaliqgant merged 3 commits into
mainfrom
feat/telegram-agents
Jun 22, 2026
Merged

feat(telegram): Telegram versions of inbox-buddy, joke-bot, spotify-releases, hn-monitor#86
khaliqgant merged 3 commits into
mainfrom
feat/telegram-agents

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Jun 22, 2026

Copy link
Copy Markdown
Member

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-helpers telegramClient() (the generic providerClient; there's no ergonomic telegram client yet — see relayfile-adapters#222). Provides message parsing, bareChatId (__title suffix strip), conversation keying, bot-loop / wrong-chat / empty guards, and a send() / replyToMessage() wrapper that threads natively via reply_to_message_id. (tsconfig now includes 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 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 recognize telegram is 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.2 for simple, idempotent writeback; added READMEs and graphics for each persona.

  • New Features

    • shared/telegram.ts: wrapper over telegramClient().sendMessage() with parsing, bareChatId, conversation keys, loop/wrong-chat/empty guards, and send()/replyToMessage() (threads via reply_to_message_id and forum threadId).
    • Agents:
      • inbox-buddy-telegram: Gmail Q&A; reuses ../inbox-buddy/lib/{gmail,prompt,conversation}; trigger telegram: message; optional TELEGRAM_CHAT.
      • joke-bot-telegram: conversational jokes + daily cron; per-conversation memory; sandbox:false (HTTP writeback).
      • spotify-releases-telegram: daily new releases to TELEGRAM_CHAT; requires SPOTIFY_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.
    • Docs & assets: READMEs with launch badges and avatars/banners/cards for each Telegram persona.
    • Tests: tests/telegram-agents.test.mjs cover transport and key flows; tsconfig.json includes shared/*.ts.
  • Migration

    • Auth: Telegram via a Nango bot token (@BotFather). Ensure the deploy target registers the Telegram adapter (relayfile-adapters#222, workforce#249).
    • Scope Telegram to /telegram/chats/** so the writeback path mounts.
    • Inputs: TELEGRAM_CHAT for delivery; SPOTIFY_TOKEN required for spotify-releases.

Written for commit e9be003. Summary will update on new commits.

Review in cubic

…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>
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a shared Telegram transport module (shared/telegram.ts) and four new Telegram-variant agents—joke-bot-telegram, spotify-releases-telegram, inbox-buddy-telegram, and hn-monitor-telegram—each with a persona configuration, agent handler, and README. A unified test suite covers all agents and transport helpers, and the relay-helpers dependency is bumped to support the new functionality.

Changes

Telegram Agent Variants

Layer / File(s) Summary
Shared Telegram transport
shared/telegram.ts
Defines TelegramMessage, TelegramSender, payload parsing (readTelegramMessage), chat-id normalization (bareChatId), conversation keying, bot-echo/chat/empty-text skip guard (skipReason), defaultTelegram() implementation via telegramClient().messages.write, and replyToMessage() with no-receipt logging.
joke-bot-telegram agent and persona
joke-bot-telegram/README.md, joke-bot-telegram/persona.ts, joke-bot-telegram/agent.ts
Persona configures Claude haiku, 30-day workspace memory, and Telegram integration scope. Agent handles a daily cron posting a topical joke and Telegram message events replying in-thread with multi-turn callback humor; each turn is persisted to workspace memory.
spotify-releases-telegram agent and persona
spotify-releases-telegram/README.md, spotify-releases-telegram/persona.ts, spotify-releases-telegram/agent.ts
Persona is cron-only with no LLM and 30-day memory. Agent daily fetches followed Spotify artists, filters releases newer than a durable last-check date, renders plain-text bullet lines, and sends to Telegram.
inbox-buddy-telegram agent and persona
inbox-buddy-telegram/README.md, inbox-buddy-telegram/persona.ts, inbox-buddy-telegram/agent.ts
Persona configures Google-mail + Telegram integrations with sandbox and 60-day memory. Agent parses incoming Telegram messages, loads Gmail threads from a VFS mount, builds a grounded LLM prompt with 45 s timeout and deterministic fallback, persists the turn transcript, and sends a threaded reply.
hn-monitor-telegram agent and persona
hn-monitor-telegram/README.md, hn-monitor-telegram/persona.ts, hn-monitor-telegram/agent.ts
Persona uses Claude haiku with 30-day memory and Q&A mode. Cron fetches HN front page, filters by TOPICS, deduplicates via a durable seen-set with at-least-once retry safety (claim before post, release on pre-header failure), posts a threaded digest. Telegram message trigger performs LLM Q&A over recalled digests with title-list fallback.
Test suite and dependency updates
tests/telegram-agents.test.mjs, tsconfig.json, package.json
Adds makeCtx/makeTelegram/telegramEvent helpers, shared transport unit tests (readTelegramMessage, bareChatId, conversationKeyForTelegram, skipReason, replyToMessage), and integration-style tests for each agent's primary flow, guard paths, and failure modes. Updates tsconfig include glob and bumps @relayfile/relay-helpers to ^0.4.2.

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)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Poem

🐇 Hop, hop—four bots now chat on Telegram's wire,
Jokes and digests and inbox replies entire.
A shared transport module keeps the sending neat,
With threading and receipts to make each message complete.
The rabbit typed the tests and checked them twice—
New agents deployed, and oh, they're so nice! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.33% 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
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding Telegram versions of four agents (inbox-buddy, joke-bot, spotify-releases, hn-monitor).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description is clearly related to the changeset, detailing the addition of Telegram-transport siblings for four agents with shared transport logic.

✏️ 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/telegram-agents

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 and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment on lines +146 to +152
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(() => [])
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

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

Suggested change
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();
}

Comment thread spotify-releases-telegram/agent.ts Outdated
Comment on lines +45 to +47
const releases = perArtist
.flatMap((r) => (r.status === 'fulfilled' ? r.value : []))
.filter((rel) => rel.date > since);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

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.

Comment thread joke-bot-telegram/agent.ts Outdated
Comment on lines +92 to +94
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

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.

Suggested change
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);

Comment on lines +86 to +91
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')}`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

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.

Suggested change
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;
}

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread spotify-releases-telegram/agent.ts Outdated
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 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment thread spotify-releases-telegram/agent.ts Outdated
Comment on lines +103 to +107
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' });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

@agent-relay-code

Copy link
Copy Markdown
Contributor

Review: PR #86 — Telegram siblings of inbox-buddy, joke-bot, spotify-releases, hn-monitor

Summary

Additive feature PR. Creates 4 new *-telegram agent directories (each: README.md, agent.ts, persona.ts), a new shared/telegram.ts transport, a new test file, and one tsconfig.json change to include shared/*.ts in compilation. No existing source files are modified, so blast radius is contained.

I traced every reuse: inbox-buddy-telegram imports loadConversation/recordTurn (conversation.ts), loadRecentThreads (gmail.ts), buildPrompt/focusedThreadIds/SYSTEM_PROMPT (prompt.ts) — all signatures match, and the mount-resolution pattern (resolveMountRoot({})loadRecentThreads({ relayfileMountRoot: root }, …)) is identical to the production inbox-buddy/agent.ts. shared/telegram.ts faithfully mirrors inbox-buddy/lib/slack.ts (parse → guard → key → reply), with native Telegram threading via reply_to_message_id.

Verification

  • npm run typecheck (tsc --noEmit): clean.
  • The new tests/telegram-agents.test.mjs (all 14 cases): pass. This includes the fail-loud no-receipt header path and the at-least-once seen-claim/release logic in postFreshStories.
  • The tsconfig.json include addition is required: the test imports ../.test-build/shared/telegram.js, which only emits if shared/*.ts is compiled. Correct and necessary.

Note on the full npm test run (not a PR regression)

The canonical npm test shows 4 failing tests, but all 4 are in files this PR does not touch (tests/inbox-buddy.test.mjs, tests/hn-monitor.test.mjs). I reproduced them on the base commit 6d72d0a (a git worktree, since removed) and they fail identically there. Root cause is environmental, not code: this sandbox sets WORKFORCE_SANDBOX_ROOT/WORKFORCE_WORKSPACE_DIR=/home/daytona/workspace, which @relayfile/adapter-core's resolveMountRoot ranks above the RELAYFILE_MOUNT_ROOT those tests set, so the seeded Gmail mount isn't read. With those two vars unset, the new telegram test's Gmail case also passes, confirming the new code is correct and the failures are pre-existing sandbox noise. I did not modify any test or any unrelated file to mask this — that would be a human decision and out of this PR's scope.

Mechanical fixes applied

None needed — no lint/format/spelling/import-order issues found in the diff.

Addressed comments

  • No bot or human review comments were present in .workforce/context.json or the provided PR metadata, so there were none to reconcile.

Advisory Notes

  • The resolveMountRoot env-priority interaction that makes the existing inbox-buddy/hn-monitor mount tests flaky under WORKFORCE_SANDBOX_ROOT is worth a follow-up (test setup could override via the client option rather than only RELAYFILE_MOUNT_ROOT), but it predates this PR and touches files outside its scope — left unchanged here.

No code changes were left in the working tree (build artifacts cleaned, base worktree removed; git status clean). The PR is additive, typechecks, and ships passing tests for everything it introduces. The remaining npm test red is a pre-existing environment artifact in unrelated files, not introduced by this change — so this still needs a human to decide how to treat that unrelated red before merge.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
tests/telegram-agents.test.mjs (1)

219-250: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6d72d0a and 91473af.

📒 Files selected for processing (15)
  • hn-monitor-telegram/README.md
  • hn-monitor-telegram/agent.ts
  • hn-monitor-telegram/persona.ts
  • inbox-buddy-telegram/README.md
  • inbox-buddy-telegram/agent.ts
  • inbox-buddy-telegram/persona.ts
  • joke-bot-telegram/README.md
  • joke-bot-telegram/agent.ts
  • joke-bot-telegram/persona.ts
  • shared/telegram.ts
  • spotify-releases-telegram/README.md
  • spotify-releases-telegram/agent.ts
  • spotify-releases-telegram/persona.ts
  • tests/telegram-agents.test.mjs
  • tsconfig.json

Comment thread hn-monitor-telegram/agent.ts
Comment thread hn-monitor-telegram/agent.ts
Comment thread spotify-releases-telegram/agent.ts
Comment thread spotify-releases-telegram/agent.ts
…-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>
@agent-relay-code

Copy link
Copy Markdown
Contributor

Review of PR #86 — Telegram sibling agents

Summary

PR #86 adds four Telegram-transport sibling agents (hn-monitor-telegram, inbox-buddy-telegram, joke-bot-telegram, spotify-releases-telegram), a shared/telegram.ts transport, bumps @relayfile/relay-helpers ^0.4.1 → ^0.4.2 (pulling adapter-core ^0.4.3), and adds shared/*.ts to tsconfig.json include + a new tests/telegram-agents.test.mjs. This is additive — all source files are new; no existing agent code is modified.

Verification (the way CI runs it)

  • npx tsc --noEmit (the typecheck script): passes clean.
  • npm test build step (tsc --noEmit false --outDir .test-build): compiles clean.
  • New tests/telegram-agents.test.mjs: 14/14 pass.
  • Full suite: 152/156 pass. The 4 failures are pre-existing and unrelated to this PR — they live in tests/inbox-buddy.test.mjs (3) and tests/hn-monitor.test.mjs (1), neither of which the PR touches, and neither imports any new shared//*-telegram code.

Root cause of those 4 failures is environmental, not a PR defect: the harness sets WORKFORCE_SANDBOX_ROOT=/home/daytona/workspace, and @relayfile/adapter-core's resolveMountRoot checks WORKFORCE_SANDBOX_ROOT (and other roots) before the RELAYFILE_MOUNT_ROOT those tests use to pin a temp Gmail mount. Their handlers call resolveMountRoot({}) with no client option, so the temp mount is shadowed and listJsonFiles reads an empty dir. Unsetting WORKFORCE_SANDBOX_ROOT makes the new PR test that exercises the same path (inbox-buddy-telegram: answers a Gmail question…) pass; the remaining 4 are pre-existing Slack/hn tests outside this PR's scope. I did not modify them (changing tests to pass is a human decision, and they aren't this PR's responsibility).

Findings

No code changes were warranted. Specific things checked and cleared:

  • API parity: telegramClient().sendMessage(chatId, text, { replyToMessageId, threadId }) and { ok, messageId } match @relayfile/relay-helpers@0.4.2's TelegramClient/TelegramMessageResult; telegramClient({ writebackTimeoutMs }) is a valid IntegrationClientOptions. All confirmed against installed type defs.
  • Reused libs: inbox-buddy-telegram imports loadConversation/recordTurn, loadRecentThreads, buildPrompt/focusedThreadIds/SYSTEM_PROMPT — all exist with matching signatures.
  • Lockfile: package.json/package-lock.json bumps are consistent; npm install resolves cleanly.
  • postFreshStories idempotency (safety-critical at-least-once logic): claim-before-post, release-and-rethrow only while nothing has landed, keep-claim-and-log after the header posts. This is correct fail-closed behavior — left unchanged intentionally.
  • No typos/spelling/import-order issues in the new files.

Addressed comments

  • No bot or human review comments were available to this run (.workforce/ contained only pr.diff, changed-files.txt, and context.json; context.json carries no review threads). Nothing to address.

Advisory Notes

  • The 4 pre-existing failures in tests/inbox-buddy.test.mjs and tests/hn-monitor.test.mjs will surface red whenever a runner has WORKFORCE_SANDBOX_ROOT (or WORKSPACE_ROOT/RELAYFILE_MOUNT_PATH) set, because those tests rely on RELAYFILE_MOUNT_ROOT which adapter-core's resolveMountRoot ranks below those vars. This is unrelated to PR feat(telegram): Telegram versions of inbox-buddy, joke-bot, spotify-releases, hn-monitor #86 and out of its scope, but a human may want a follow-up: either have those handlers/tests pass relayfileMountRoot explicitly (highest precedence) or have the test harness clear the higher-precedence roots. I left the code unchanged.

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

@khaliqgant khaliqgant merged commit aedc47b into main Jun 22, 2026
2 checks passed
@khaliqgant khaliqgant deleted the feat/telegram-agents branch June 22, 2026 20:41
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.

1 participant