Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions src/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,22 +178,24 @@ export class WorkCoordinator {
return curateTrending(newRepos, feedback)
}

// Step 4: deliver the digest via Telegram. Returns whether the send succeeded.
private static async sendTelegram(repos: CuratedRepo[]): Promise<boolean> {
// Step 4: deliver the digest via Telegram. Returns the repo names that went out.
private static async sendTelegram(repos: CuratedRepo[]): Promise<Set<string>> {
logger.info('⚡️ Sending trending digest via Telegram...')

try {
await trendingTelegramTool.invoke({ repos })
return true
const raw = await trendingTelegramTool.invoke({ repos })
const { delivered } = parseJson<{ delivered?: string[] }>(raw, {})

return new Set(delivered ?? [])
} catch (err) {
logger.error({ err }, '❌ Telegram delivery failed')
await notifyError('Trending Telegram delivery', err)
return false
return new Set()
}
}

// Step 5: persist the curated repos, tagging whether delivery succeeded.
private static async saveTrending(repos: CuratedRepo[], sent: boolean, now: string): Promise<void> {
// Step 5: persist the curated repos, tagging each with whether it was delivered.
private static async saveTrending(repos: CuratedRepo[], delivered: Set<string>, now: string): Promise<void> {
try {
await saveTrendingRepos(
repos.map((r) => ({
Expand All @@ -205,12 +207,12 @@ export class WorkCoordinator {
today_stars: r.today_stars,
summary: r.summary,
tags: r.tags,
sent,
sent: delivered.has(r.repo_name),
created_at: now,
updated_at: now,
}))
)
logger.info(`✅ Saved ${repos.length} trending repos to database.`)
logger.info(`✅ Saved ${repos.length} trending repos to database (${delivered.size} marked sent).`)
} catch (err) {
logger.error({ err }, '❌ Failed to save trending repos')
await notifyError('saveTrendingRepos', err)
Expand Down Expand Up @@ -366,10 +368,10 @@ export class WorkCoordinator {
if (!curated) return

// ── Step 4: Send via Telegram ───────────────────────────────────────────
const sent = await WorkCoordinator.sendTelegram(curated)
const delivered = await WorkCoordinator.sendTelegram(curated)

// ── Step 5: Save to DB ──────────────────────────────────────────────────
await WorkCoordinator.saveTrending(curated, sent, now)
await WorkCoordinator.saveTrending(curated, delivered, now)

sectionLogger(`✅ GitHub Trending job complete for ${today}`)
}
Expand Down
30 changes: 29 additions & 1 deletion src/tools/news-telegram.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { buildTrendingMessage } from './news-telegram.tool.ts'
import { buildTrendingMessage, fitRepos } from './news-telegram.tool.ts'
import { TELEGRAM_MAX_CHARS, TRENDING_SUMMARY_MAX, TRENDING_TAG_MAX, TRENDING_TAGS_MAX, TRENDING_TOP_N } from '../constants/index.ts'
import type { CuratedRepo } from '../schemas/index.ts'

Expand Down Expand Up @@ -90,3 +90,31 @@ test('drops trailing repos rather than emitting a message Telegram will reject',

assert.ok(message.length <= TELEGRAM_MAX_CHARS, `message was ${message.length} chars`)
})

// A dropped repo is never delivered, so the caller has to be able to tell which
// ones went out — otherwise they get recorded as sent.
test('reports which repos survived the drop, not just the message', () => {
const bomb = Array.from({ length: TRENDING_TOP_N }, (_, i) =>
repo({ repo_name: `owner${i}/project-${i}`, summary: '&'.repeat(TRENDING_SUMMARY_MAX) })
)

const kept = fitRepos(bomb, '2026-08-01')

assert.ok(kept.length < bomb.length, 'this input must actually trigger the drop')
assert.equal(buildTrendingMessage(bomb, '2026-08-01'), assembleOf(kept))

// Every kept repo appears in the message; every dropped one does not.
const message = buildTrendingMessage(bomb, '2026-08-01')
const dropped = bomb.slice(kept.length)

for (const r of kept) assert.ok(message.includes(r.repo_name), `${r.repo_name} should be in the digest`)
for (const r of dropped) assert.ok(!message.includes(r.repo_name), `${r.repo_name} was dropped and must not appear`)
})

test('keeps every repo when the digest fits', () => {
const fits = [repo(), repo({ repo_name: 'vuejs/core' })]

assert.equal(fitRepos(fits, '2026-08-01').length, 2)
})

const assembleOf = (kept: CuratedRepo[]) => buildTrendingMessage(kept, '2026-08-01')
24 changes: 20 additions & 4 deletions src/tools/news-telegram.tool.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DynamicStructuredTool } from '@langchain/core/tools'
import { z } from 'zod'
import { logger } from '../utils/logger.js'
import { escapeHtml, truncate } from '../agent/utils.js'
import { sendTelegramMessage } from './telegram.js'
import { TELEGRAM_MAX_CHARS, TRENDING_SUMMARY_MAX, TRENDING_TAG_MAX, TRENDING_TAGS_MAX } from '../constants/index.js'
Expand Down Expand Up @@ -42,12 +43,16 @@ function assemble(repos: CuratedRepo[], today: string): string {
].join('\n')
}

export function buildTrendingMessage(repos: CuratedRepo[], today: string): string {
export function fitRepos(repos: CuratedRepo[], today: string): CuratedRepo[] {
let kept = repos

while (kept.length > 1 && assemble(kept, today).length > TELEGRAM_MAX_CHARS) kept = kept.slice(0, -1)

return assemble(kept, today)
return kept
}

export function buildTrendingMessage(repos: CuratedRepo[], today: string): string {
return assemble(fitRepos(repos, today), today)
}

export const trendingTelegramTool = new DynamicStructuredTool({
Expand All @@ -71,8 +76,19 @@ export const trendingTelegramTool = new DynamicStructuredTool({
}),
func: async ({ repos }) => {
const today = new Date().toISOString().split('T')[0]
const chatId = await sendTelegramMessage(buildTrendingMessage(repos, today), 'Trending repos')
const kept = fitRepos(repos, today)

if (kept.length < repos.length) {
logger.warn(
`✂️ Digest too long for Telegram — dropped ${repos.length - kept.length} of ${repos.length} repos: ${repos
.slice(kept.length)
.map((r) => r.repo_name)
.join(', ')}`
)
}

const chatId = await sendTelegramMessage(assemble(kept, today), 'Trending repos')

return JSON.stringify({ success: true, chat_id: chatId, date: today })
return JSON.stringify({ success: true, chat_id: chatId, date: today, delivered: kept.map((r) => r.repo_name) })
},
})
Loading