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
167 changes: 0 additions & 167 deletions src/services/SessionManager.ts

This file was deleted.

128 changes: 92 additions & 36 deletions src/services/TelegramService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
*/

import {
type BaseSessionService,
createSamplingHandler,
type EnhancedRunner,
extractTextFromContent,
type LlmRequest,
type MemoryService,
} from "@iqai/adk";
import { createClawdAgent } from "../agents/agent.js";
import { getTelegramAgent } from "../agents/telegram-agent/agent.js";
Expand All @@ -19,7 +22,6 @@ import {
initScheduler,
stopScheduler,
} from "./SchedulerService.js";
import { sessionManager } from "./SessionManager.js";

const log = createLogger("Telegram");

Expand Down Expand Up @@ -227,51 +229,115 @@ adk-claw pairing approve telegram ${code}
Code expires in 1 hour.`;
}

const commands: Record<string, CommandHandler> = {
"/start": async (chatId, userId) => {
const config = getRawConfig();
/**
* Format duration between a unix timestamp (seconds) and now
*/
function formatDuration(startTimestamp: number): string {
const diffMs = Date.now() - startTimestamp * 1000;
const minutes = Math.floor(diffMs / 60000);
const hours = Math.floor(minutes / 60);

if (hours > 0) {
return `${hours}h ${minutes % 60}m`;
}
return `${minutes}m`;
}

/**
* Create command handlers bound to ADK session lifecycle.
* Uses sessionService.createSession / deleteSession and runner.setSession
* to properly create/destroy ADK sessions instead of local state.
*/
function createCommands(deps: {
runner: EnhancedRunner;
sessionService: BaseSessionService;
memoryService?: MemoryService;
}): Record<string, CommandHandler> {
const { runner, sessionService, memoryService } = deps;

return {
"/start": async (_chatId, userId) => {
const config = getRawConfig();

// Check if user is already allowed
if (isAllowed("telegram", userId)) {
return `👋 Welcome back!
// Check if user is already allowed
if (isAllowed("telegram", userId)) {
return `👋 Welcome back!

I'm ${config.agent.name}, your personal AI assistant.

Commands:
/new - Save session & start fresh
/reset - Clear session without saving
/help - Show available commands`;
}
}

// User needs pairing
return getPairingResponse(userId, undefined, _chatId);
},

// User needs pairing
return getPairingResponse(userId, undefined, chatId);
},
"/new": async (_chatId, _userId) => {
const currentSession = runner.getSession();

"/new": async (chatId, _userId) => {
const summary = await sessionManager.saveAndReset(chatId);
return `✅ Session saved to memory.
// Save current session to memory if it has events
if (memoryService && currentSession.events.length > 0) {
await memoryService.addSessionToMemory(currentSession);
}

// Derive summary from ADK session events
const eventCount = currentSession.events.length;
let summary = "No conversation to save.";
if (eventCount > 0) {
const firstTimestamp = currentSession.events[0].timestamp;
const duration = formatDuration(firstTimestamp);
summary = `${eventCount} events over ${duration}`;
}

// Create a new ADK session and swap it into the runner
const newSession = await sessionService.createSession(
currentSession.appName,
currentSession.userId,
);
runner.setSession(newSession);
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

Calling runner.setSession(newSession) on a shared runner instance will globally change the active session for all Telegram users. If one user triggers the /new command, it will effectively reset the conversation for every other user currently interacting with the bot. This is a significant issue if the application is intended for multi-user scenarios, where session state must be isolated per chatId. However, if this application is designed for single-user or paired-user contexts (e.g., a personal bot), this behavior might be acceptable as per repository guidelines.

References
  1. In applications designed for single-user or paired-user contexts (e.g., personal bots), using a hardcoded session user ID is acceptable, as the risks associated with multi-user data crossover are not present.


return `✅ Session saved to memory.

📝 Summary: ${summary}

🆕 New session started!`;
},
},

"/reset": async (_chatId, _userId) => {
const currentSession = runner.getSession();

// Delete the old session without saving to memory
await sessionService.deleteSession(
currentSession.appName,
currentSession.userId,
currentSession.id,
);

// Create a fresh ADK session
const newSession = await sessionService.createSession(
currentSession.appName,
currentSession.userId,
);
runner.setSession(newSession);
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

Similar to the /new command, using runner.setSession here affects the global state of the shared runner. This will cause a session reset for all users simultaneously if any single user runs the /reset command. This is a significant issue if the application is intended for multi-user scenarios, where session state must be isolated per chatId. However, if this application is designed for single-user or paired-user contexts (e.g., a personal bot), this behavior might be acceptable as per repository guidelines.

References
  1. In applications designed for single-user or paired-user contexts (e.g., personal bots), using a hardcoded session user ID is acceptable, as the risks associated with multi-user data crossover are not present.


"/reset": async (chatId, _userId) => {
sessionManager.reset(chatId);
return "🔄 Session cleared (not saved). Fresh start!";
},
return "🔄 Session cleared (not saved). Fresh start!";
},

"/help": async (_chatId, _userId) => {
return `📚 Available commands:
"/help": async (_chatId, _userId) => {
return `📚 Available commands:

/start - Start the bot and pair
/new - Save session & start fresh
/reset - Clear session without saving
/help - Show this help message

Just send a message to chat with me!`;
},
};
},
};
}

/**
* Start the Telegram bot with MCP sampling
Expand All @@ -291,10 +357,8 @@ export async function startTelegramBot(): Promise<void> {
channel: "telegram",
});

// Wire memory service into session manager for /new command
if (memoryService) {
sessionManager.setDeps(memoryService, sessionService, session);
}
// Create command handlers bound to ADK session lifecycle
const commands = createCommands({ runner, sessionService, memoryService });

// Create sampling handler with command detection
const samplingHandler = createSamplingHandler(
Expand Down Expand Up @@ -327,17 +391,9 @@ export async function startTelegramBot(): Promise<void> {
registerUserCommands(config.telegramBotToken, chatId);
}

// Track in session
sessionManager.getOrCreate(chatId, userId);
sessionManager.addMessage(chatId, "user", messageText);

// Get response from agent
// Get response from agent (ADK tracks events in session automatically)
try {
const response = await runner.ask(messageText);
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 removal of SessionManager has eliminated the isolation of conversations between different Telegram chats. Since runner.ask() uses the session currently set on the runner, and there is only one runner instance for the entire service, all users and groups will now share the same conversation history. This is a regression from the previous implementation which used chatId to isolate sessions. This is a significant issue for privacy and correctness if the application is intended for multi-user or multi-group scenarios. To fix this while using ADK's session management, you should ensure that the runner uses a specific sessionId (e.g., the chatId) for each request, likely by using runner.runAsync(messageText, { sessionId: chatId }) and extracting the response, rather than the simplified ask() method which relies on a single global session context. However, if this application is designed for single-user or paired-user contexts (e.g., a personal bot), this shared state might be acceptable as per repository guidelines.

References
  1. In applications designed for single-user or paired-user contexts (e.g., personal bots), using a hardcoded session user ID is acceptable, as the risks associated with multi-user data crossover are not present.


// Track response
sessionManager.addMessage(chatId, "assistant", response);

return response;
} catch (error) {
log.error(
Expand Down
1 change: 0 additions & 1 deletion src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@
*/

export { initScheduler, stopScheduler } from "./SchedulerService.js";
export { sessionManager } from "./SessionManager.js";
export { startTelegramBot } from "./TelegramService.js";