From f60b723f4e690cf77162fe8063afd20288126365 Mon Sep 17 00:00:00 2001 From: Cameron Date: Wed, 11 Mar 2026 16:58:38 -0700 Subject: [PATCH] fix(telegram): simplify command responses and enrich status output (#565) Co-authored-by: Letta Code --- src/channels/telegram.ts | 78 +++++++++++++++++++++++++++++++++--- src/core/bot.status.test.ts | 80 +++++++++++++++++++++++++++++++++++++ src/core/bot.ts | 17 +++++++- 3 files changed, 168 insertions(+), 7 deletions(-) create mode 100644 src/core/bot.status.test.ts diff --git a/src/channels/telegram.ts b/src/channels/telegram.ts index 93e6655..6b03135 100644 --- a/src/channels/telegram.ts +++ b/src/channels/telegram.ts @@ -26,6 +26,16 @@ import { resolveDailyLimits, checkDailyLimit, type GroupModeConfig } from './gro import { createLogger } from '../logger.js'; const log = createLogger('Telegram'); +const KNOWN_TELEGRAM_COMMANDS = new Set([ + 'status', + 'model', + 'heartbeat', + 'reset', + 'cancel', + 'setconv', + 'help', + 'start', +]); function getTelegramErrorReason(err: unknown): string { if (err && typeof err === 'object') { @@ -244,6 +254,9 @@ export class TelegramAdapter implements ChannelAdapter { "*LettaBot* - AI assistant with persistent memory\n\n" + "*Commands:*\n" + "/status - Show current status\n" + + "/model - Show current model and recommendations\n" + + "/reset - Reset conversation\n" + + "/cancel - Cancel active run\n" + "/help - Show this message\n\n" + "Just send me a message to get started!", { parse_mode: 'Markdown' } @@ -254,7 +267,15 @@ export class TelegramAdapter implements ChannelAdapter { this.bot.command('status', async (ctx) => { if (this.onCommand) { const result = await this.onCommand('status', String(ctx.chat.id)); - await ctx.reply(result || 'No status available'); + const replyToMessageId = + 'message' in ctx && ctx.message + ? String(ctx.message.message_id) + : undefined; + await this.sendMessage({ + chatId: String(ctx.chat.id), + text: result || 'No status available', + replyToMessageId, + }); } }); @@ -269,14 +290,32 @@ export class TelegramAdapter implements ChannelAdapter { this.bot.command('reset', async (ctx) => { if (this.onCommand) { const result = await this.onCommand('reset', String(ctx.chat.id)); - await ctx.reply(result || 'Reset complete'); + const replyToMessageId = + 'message' in ctx && ctx.message + ? String(ctx.message.message_id) + : undefined; + await this.sendMessage({ + chatId: String(ctx.chat.id), + text: result || 'Reset complete', + replyToMessageId, + }); } }); this.bot.command('cancel', async (ctx) => { if (this.onCommand) { const result = await this.onCommand('cancel', String(ctx.chat.id)); - if (result) await ctx.reply(result); + if (result) { + const replyToMessageId = + 'message' in ctx && ctx.message + ? String(ctx.message.message_id) + : undefined; + await this.sendMessage({ + chatId: String(ctx.chat.id), + text: result, + replyToMessageId, + }); + } } }); @@ -285,7 +324,15 @@ export class TelegramAdapter implements ChannelAdapter { if (this.onCommand) { const args = ctx.match?.trim() || undefined; const result = await this.onCommand('model', String(ctx.chat.id), args); - await ctx.reply(result || 'No model info available'); + const replyToMessageId = + 'message' in ctx && ctx.message + ? String(ctx.message.message_id) + : undefined; + await this.sendMessage({ + chatId: String(ctx.chat.id), + text: result || 'No model info available', + replyToMessageId, + }); } }); @@ -294,7 +341,15 @@ export class TelegramAdapter implements ChannelAdapter { if (this.onCommand) { const args = ctx.match?.trim() || undefined; const result = await this.onCommand('setconv', String(ctx.chat.id), args); - await ctx.reply(result || 'Failed to set conversation'); + const replyToMessageId = + 'message' in ctx && ctx.message + ? String(ctx.message.message_id) + : undefined; + await this.sendMessage({ + chatId: String(ctx.chat.id), + text: result || 'Failed to set conversation', + replyToMessageId, + }); } }); @@ -305,7 +360,18 @@ export class TelegramAdapter implements ChannelAdapter { const text = ctx.message.text; if (!userId) return; - if (text.startsWith('/')) return; // Skip other commands + if (text.startsWith('/')) { + const commandToken = text.slice(1).trim().split(/\s+/)[0] || ''; + const commandName = commandToken.toLowerCase().split('@')[0]; + if (!KNOWN_TELEGRAM_COMMANDS.has(commandName)) { + await this.sendMessage({ + chatId: String(chatId), + text: `Unknown command: /${commandName || '(empty)'}\nTry /help.`, + replyToMessageId: String(ctx.message.message_id), + }); + } + return; + } // Group gating (runs AFTER pairing middleware) const gating = this.applyGroupGating(ctx); diff --git a/src/core/bot.status.test.ts b/src/core/bot.status.test.ts new file mode 100644 index 0000000..daf467d --- /dev/null +++ b/src/core/bot.status.test.ts @@ -0,0 +1,80 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { MockChannelAdapter } from '../test/mock-channel.js'; +import { LettaBot } from './bot.js'; + +describe('LettaBot /status command', () => { + let dataDir: string; + let workingDir: string; + const originalDataDir = process.env.DATA_DIR; + const originalBaseUrl = process.env.LETTA_BASE_URL; + + beforeEach(() => { + dataDir = mkdtempSync(join(tmpdir(), 'lettabot-data-')); + workingDir = mkdtempSync(join(tmpdir(), 'lettabot-work-')); + + process.env.DATA_DIR = dataDir; + delete process.env.LETTA_BASE_URL; + }); + + afterEach(() => { + if (originalDataDir === undefined) { + delete process.env.DATA_DIR; + } else { + process.env.DATA_DIR = originalDataDir; + } + + if (originalBaseUrl === undefined) { + delete process.env.LETTA_BASE_URL; + } else { + process.env.LETTA_BASE_URL = originalBaseUrl; + } + + rmSync(dataDir, { recursive: true, force: true }); + rmSync(workingDir, { recursive: true, force: true }); + }); + + it('includes conversation and runtime fields', async () => { + writeFileSync( + join(dataDir, 'lettabot-agent.json'), + JSON.stringify( + { + version: 2, + agents: { + LettaBot: { + agentId: 'agent-test-123', + conversationId: 'conv-shared-123', + conversations: { + telegram: 'conv-tg-1', + }, + createdAt: '2026-01-01T00:00:00.000Z', + lastUsedAt: '2026-01-01T00:00:01.000Z', + }, + }, + }, + null, + 2, + ), + 'utf-8', + ); + + const bot = new LettaBot({ + workingDir, + allowedTools: [], + memfs: true, + }); + const adapter = new MockChannelAdapter(); + bot.registerChannel(adapter); + + const response = await adapter.simulateMessage('/status'); + + expect(response).toContain('Agent ID: `agent-test-123`'); + expect(response).toContain('Conversation ID: `conv-shared-123`'); + expect(response).toContain('Conversation keys: telegram'); + expect(response).toContain('Memfs: on'); + expect(response).toContain('Server: https://api.letta.com'); + expect(response).toContain('Channels: mock'); + }); +}); diff --git a/src/core/bot.ts b/src/core/bot.ts index 622a191..8a4c094 100644 --- a/src/core/bot.ts +++ b/src/core/bot.ts @@ -672,12 +672,27 @@ export class LettaBot implements AgentSession { switch (command) { case 'status': { const info = this.store.getInfo(); + const memfsState = this.config.memfs === true + ? 'on' + : this.config.memfs === false + ? 'off' + : 'unknown'; + const serverUrl = info.baseUrl || process.env.LETTA_BASE_URL || 'https://api.letta.com'; + const conversationId = info.conversationId || '(none)'; + const conversationKeys = info.conversations + ? Object.keys(info.conversations).sort() + : []; + const channels = Array.from(this.channels.keys()); const lines = [ `*Status*`, `Agent ID: \`${info.agentId || '(none)'}\``, + `Conversation ID: \`${conversationId}\``, + `Conversation keys: ${conversationKeys.length > 0 ? conversationKeys.join(', ') : '(none)'}`, + `Memfs: ${memfsState}`, + `Server: ${serverUrl}`, `Created: ${info.createdAt || 'N/A'}`, `Last used: ${info.lastUsedAt || 'N/A'}`, - `Channels: ${Array.from(this.channels.keys()).join(', ')}`, + `Channels: ${channels.length > 0 ? channels.join(', ') : '(none)'}`, ]; return lines.join('\n'); }