fix(telegram): simplify command responses and enrich status output (#565)

Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
Cameron
2026-03-11 16:58:38 -07:00
committed by GitHub
parent fb32a4c3cd
commit f60b723f4e
3 changed files with 168 additions and 7 deletions

View File

@@ -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);

View File

@@ -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');
});
});

View File

@@ -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');
}