container lifecycle management

This commit is contained in:
2026-03-12 15:13:38 -04:00
parent e99ef5d2dd
commit b9cc397e05
61 changed files with 6880 additions and 31 deletions

View File

@@ -0,0 +1,163 @@
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import type { Authenticator } from '../auth/authenticator.js';
import { AgentHarness } from '../harness/agent-harness.js';
import type { InboundMessage } from '../types/messages.js';
import { randomUUID } from 'crypto';
import type { ProviderConfig } from '../llm/provider.js';
export interface TelegramHandlerConfig {
authenticator: Authenticator;
providerConfig: ProviderConfig;
telegramBotToken: string;
}
interface TelegramUpdate {
update_id: number;
message?: {
message_id: number;
from: {
id: number;
first_name: string;
username?: string;
};
chat: {
id: number;
type: string;
};
text?: string;
photo?: Array<{
file_id: string;
file_size: number;
}>;
};
}
/**
* Telegram webhook handler
*/
export class TelegramHandler {
private config: TelegramHandlerConfig;
private sessions = new Map<string, AgentHarness>();
constructor(config: TelegramHandlerConfig) {
this.config = config;
}
/**
* Register Telegram webhook routes
*/
register(app: FastifyInstance): void {
app.post('/webhook/telegram', async (request: FastifyRequest, reply: FastifyReply) => {
await this.handleWebhook(request, reply, app);
});
}
/**
* Handle Telegram webhook
*/
private async handleWebhook(
request: FastifyRequest,
reply: FastifyReply,
app: FastifyInstance
): Promise<void> {
const logger = app.log;
try {
const update = request.body as TelegramUpdate;
if (!update.message?.text) {
// Ignore non-text messages for now
reply.code(200).send({ ok: true });
return;
}
const telegramUserId = update.message.from.id.toString();
const chatId = update.message.chat.id;
const text = update.message.text;
logger.info({ telegramUserId, chatId, text }, 'Received Telegram message');
// Authenticate
const authContext = await this.config.authenticator.authenticateTelegram(telegramUserId);
if (!authContext) {
logger.warn({ telegramUserId }, 'Telegram user not authenticated');
await this.sendTelegramMessage(
chatId,
'Please link your Telegram account to Dexorder first.'
);
reply.code(200).send({ ok: true });
return;
}
// Get or create harness
let harness = this.sessions.get(authContext.sessionId);
if (!harness) {
harness = new AgentHarness({
userId: authContext.userId,
sessionId: authContext.sessionId,
license: authContext.license,
providerConfig: this.config.providerConfig,
logger,
});
await harness.initialize();
this.sessions.set(authContext.sessionId, harness);
}
// Process message
const inboundMessage: InboundMessage = {
messageId: randomUUID(),
userId: authContext.userId,
sessionId: authContext.sessionId,
content: text,
timestamp: new Date(),
};
const response = await harness.handleMessage(inboundMessage);
// Send response back to Telegram
await this.sendTelegramMessage(chatId, response.content);
reply.code(200).send({ ok: true });
} catch (error) {
logger.error({ error }, 'Error handling Telegram webhook');
reply.code(500).send({ ok: false, error: 'Internal server error' });
}
}
/**
* Send message to Telegram chat
*/
private async sendTelegramMessage(chatId: number, text: string): Promise<void> {
const url = `https://api.telegram.org/bot${this.config.telegramBotToken}/sendMessage`;
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
chat_id: chatId,
text,
parse_mode: 'Markdown',
}),
});
if (!response.ok) {
throw new Error(`Telegram API error: ${response.statusText}`);
}
} catch (error) {
console.error('Failed to send Telegram message:', error);
throw error;
}
}
/**
* Cleanup old sessions (call periodically)
*/
async cleanupSessions(maxAgeMs = 30 * 60 * 1000): Promise<void> {
// TODO: Track session last activity and cleanup
// For now, sessions persist until server restart
}
}

View File

@@ -0,0 +1,161 @@
import type { FastifyInstance, FastifyRequest } from 'fastify';
import type { WebSocket } from '@fastify/websocket';
import type { Authenticator } from '../auth/authenticator.js';
import { AgentHarness } from '../harness/agent-harness.js';
import type { InboundMessage } from '../types/messages.js';
import { randomUUID } from 'crypto';
import type { ProviderConfig } from '../llm/provider.js';
export interface WebSocketHandlerConfig {
authenticator: Authenticator;
providerConfig: ProviderConfig;
}
/**
* WebSocket channel handler
*/
export class WebSocketHandler {
private config: WebSocketHandlerConfig;
private sessions = new Map<string, AgentHarness>();
constructor(config: WebSocketHandlerConfig) {
this.config = config;
}
/**
* Register WebSocket routes
*/
register(app: FastifyInstance): void {
app.get(
'/ws/chat',
{ websocket: true },
async (socket: WebSocket, request: FastifyRequest) => {
await this.handleConnection(socket, request, app);
}
);
}
/**
* Handle WebSocket connection
*/
private async handleConnection(
socket: WebSocket,
request: FastifyRequest,
app: FastifyInstance
): Promise<void> {
const logger = app.log;
// Send initial connecting message
socket.send(
JSON.stringify({
type: 'status',
status: 'authenticating',
message: 'Authenticating...',
})
);
// Authenticate (this may take time if creating container)
const authContext = await this.config.authenticator.authenticateWebSocket(request);
if (!authContext) {
logger.warn('WebSocket authentication failed');
socket.send(
JSON.stringify({
type: 'error',
message: 'Authentication failed',
})
);
socket.close(1008, 'Authentication failed');
return;
}
logger.info(
{ userId: authContext.userId, sessionId: authContext.sessionId },
'WebSocket connection authenticated'
);
// Send workspace starting message
socket.send(
JSON.stringify({
type: 'status',
status: 'initializing',
message: 'Starting your workspace...',
})
);
// Create agent harness
const harness = new AgentHarness({
userId: authContext.userId,
sessionId: authContext.sessionId,
license: authContext.license,
providerConfig: this.config.providerConfig,
logger,
});
try {
await harness.initialize();
this.sessions.set(authContext.sessionId, harness);
// Send connected message
socket.send(
JSON.stringify({
type: 'connected',
sessionId: authContext.sessionId,
userId: authContext.userId,
licenseType: authContext.license.licenseType,
message: 'Connected to Dexorder AI',
})
);
// Handle messages
socket.on('message', async (data: Buffer) => {
try {
const payload = JSON.parse(data.toString());
if (payload.type === 'message') {
const inboundMessage: InboundMessage = {
messageId: randomUUID(),
userId: authContext.userId,
sessionId: authContext.sessionId,
content: payload.content,
attachments: payload.attachments,
timestamp: new Date(),
};
const response = await harness.handleMessage(inboundMessage);
socket.send(
JSON.stringify({
type: 'message',
...response,
})
);
}
} catch (error) {
logger.error({ error }, 'Error handling WebSocket message');
socket.send(
JSON.stringify({
type: 'error',
message: 'Failed to process message',
})
);
}
});
// Handle disconnection
socket.on('close', async () => {
logger.info({ sessionId: authContext.sessionId }, 'WebSocket disconnected');
await harness.cleanup();
this.sessions.delete(authContext.sessionId);
});
socket.on('error', (error) => {
logger.error({ error, sessionId: authContext.sessionId }, 'WebSocket error');
});
} catch (error) {
logger.error({ error }, 'Failed to initialize agent harness');
socket.close(1011, 'Internal server error');
await harness.cleanup();
}
}
}