feat: agent session management — metrics, channels, dispose (P2-006)
Add session monitoring and lifecycle improvements to the gateway: - Session metrics: createdAt, promptCount, durationMs tracking - Channel tracking: which channels (websocket, discord) are connected - REST API at /api/sessions for listing, inspecting, and destroying sessions - Fix piSession.dispose() call in destroySession (FIX-01) - WebSocket gateway now tracks channel connections per session Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,13 +3,14 @@ import { AgentService } from './agent.service.js';
|
||||
import { ProviderService } from './provider.service.js';
|
||||
import { RoutingService } from './routing.service.js';
|
||||
import { ProvidersController } from './providers.controller.js';
|
||||
import { SessionsController } from './sessions.controller.js';
|
||||
import { CoordModule } from '../coord/coord.module.js';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [CoordModule],
|
||||
providers: [ProviderService, RoutingService, AgentService],
|
||||
controllers: [ProvidersController],
|
||||
controllers: [ProvidersController, SessionsController],
|
||||
exports: [AgentService, ProviderService, RoutingService],
|
||||
})
|
||||
export class AgentModule {}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { CoordService } from '../coord/coord.service.js';
|
||||
import { ProviderService } from './provider.service.js';
|
||||
import { createBrainTools } from './tools/brain-tools.js';
|
||||
import { createCoordTools } from './tools/coord-tools.js';
|
||||
import type { SessionInfoDto } from './session.dto.js';
|
||||
|
||||
export interface AgentSessionOptions {
|
||||
provider?: string;
|
||||
@@ -25,6 +26,9 @@ export interface AgentSession {
|
||||
piSession: PiAgentSession;
|
||||
listeners: Set<(event: AgentSessionEvent) => void>;
|
||||
unsubscribe: () => void;
|
||||
createdAt: number;
|
||||
promptCount: number;
|
||||
channels: Set<string>;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -107,6 +111,9 @@ export class AgentService implements OnModuleDestroy {
|
||||
piSession,
|
||||
listeners,
|
||||
unsubscribe,
|
||||
createdAt: Date.now(),
|
||||
promptCount: 0,
|
||||
channels: new Set(),
|
||||
};
|
||||
|
||||
this.sessions.set(sessionId, session);
|
||||
@@ -143,11 +150,53 @@ export class AgentService implements OnModuleDestroy {
|
||||
return this.sessions.get(sessionId);
|
||||
}
|
||||
|
||||
listSessions(): SessionInfoDto[] {
|
||||
const now = Date.now();
|
||||
return Array.from(this.sessions.values()).map((s) => ({
|
||||
id: s.id,
|
||||
provider: s.provider,
|
||||
modelId: s.modelId,
|
||||
createdAt: new Date(s.createdAt).toISOString(),
|
||||
promptCount: s.promptCount,
|
||||
channels: Array.from(s.channels),
|
||||
durationMs: now - s.createdAt,
|
||||
}));
|
||||
}
|
||||
|
||||
getSessionInfo(sessionId: string): SessionInfoDto | undefined {
|
||||
const s = this.sessions.get(sessionId);
|
||||
if (!s) return undefined;
|
||||
return {
|
||||
id: s.id,
|
||||
provider: s.provider,
|
||||
modelId: s.modelId,
|
||||
createdAt: new Date(s.createdAt).toISOString(),
|
||||
promptCount: s.promptCount,
|
||||
channels: Array.from(s.channels),
|
||||
durationMs: Date.now() - s.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
addChannel(sessionId: string, channel: string): void {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (session) {
|
||||
session.channels.add(channel);
|
||||
}
|
||||
}
|
||||
|
||||
removeChannel(sessionId: string, channel: string): void {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (session) {
|
||||
session.channels.delete(channel);
|
||||
}
|
||||
}
|
||||
|
||||
async prompt(sessionId: string, message: string): Promise<void> {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
throw new Error(`No agent session found: ${sessionId}`);
|
||||
}
|
||||
session.promptCount += 1;
|
||||
try {
|
||||
await session.piSession.prompt(message);
|
||||
} catch (err) {
|
||||
@@ -177,7 +226,13 @@ export class AgentService implements OnModuleDestroy {
|
||||
} catch (err) {
|
||||
this.logger.error(`Failed to unsubscribe session ${sessionId}`, String(err));
|
||||
}
|
||||
try {
|
||||
session.piSession.dispose();
|
||||
} catch (err) {
|
||||
this.logger.error(`Failed to dispose piSession for ${sessionId}`, String(err));
|
||||
}
|
||||
session.listeners.clear();
|
||||
session.channels.clear();
|
||||
this.sessions.delete(sessionId);
|
||||
}
|
||||
|
||||
|
||||
14
apps/gateway/src/agent/session.dto.ts
Normal file
14
apps/gateway/src/agent/session.dto.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface SessionInfoDto {
|
||||
id: string;
|
||||
provider: string;
|
||||
modelId: string;
|
||||
createdAt: string;
|
||||
promptCount: number;
|
||||
channels: string[];
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
export interface SessionListDto {
|
||||
sessions: SessionInfoDto[];
|
||||
total: number;
|
||||
}
|
||||
39
apps/gateway/src/agent/sessions.controller.ts
Normal file
39
apps/gateway/src/agent/sessions.controller.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
NotFoundException,
|
||||
Param,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
import { AgentService } from './agent.service.js';
|
||||
|
||||
@Controller('api/sessions')
|
||||
@UseGuards(AuthGuard)
|
||||
export class SessionsController {
|
||||
constructor(private readonly agentService: AgentService) {}
|
||||
|
||||
@Get()
|
||||
list() {
|
||||
const sessions = this.agentService.listSessions();
|
||||
return { sessions, total: sessions.length };
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
const info = this.agentService.getSessionInfo(id);
|
||||
if (!info) throw new NotFoundException('Session not found');
|
||||
return info;
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async destroy(@Param('id') id: string) {
|
||||
const info = this.agentService.getSessionInfo(id);
|
||||
if (!info) throw new NotFoundException('Session not found');
|
||||
await this.agentService.destroySession(id);
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
||||
const session = this.clientSessions.get(client.id);
|
||||
if (session) {
|
||||
session.cleanup();
|
||||
this.agentService.removeChannel(session.conversationId, `websocket:${client.id}`);
|
||||
this.clientSessions.delete(client.id);
|
||||
}
|
||||
}
|
||||
@@ -97,6 +98,9 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
||||
|
||||
this.clientSessions.set(client.id, { conversationId, cleanup });
|
||||
|
||||
// Track channel connection
|
||||
this.agentService.addChannel(conversationId, `websocket:${client.id}`);
|
||||
|
||||
// Send acknowledgment
|
||||
client.emit('message:ack', { conversationId, messageId: uuid() });
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
| P2-002 | done | Phase 2 | Multi-provider support — Anthropic + Ollama | #74 | #20 |
|
||||
| P2-003 | done | Phase 2 | Agent routing engine — cost/capability matrix | #75 | #21 |
|
||||
| P2-004 | done | Phase 2 | Tool registration — brain, queue, memory tools | #76 | #22 |
|
||||
| P2-005 | in-progress | Phase 2 | @mosaic/coord — migrate from v0, gateway integration | — | #23 |
|
||||
| P2-006 | not-started | Phase 2 | Agent session management — tmux + monitoring | — | #24 |
|
||||
| P2-005 | done | Phase 2 | @mosaic/coord — migrate from v0, gateway integration | #77 | #23 |
|
||||
| P2-006 | in-progress | Phase 2 | Agent session management — tmux + monitoring | — | #24 |
|
||||
| P2-007 | not-started | Phase 2 | Verify Phase 2 — multi-provider routing works | — | #25 |
|
||||
| P3-001 | not-started | Phase 3 | apps/web scaffold — Next.js 16 + BetterAuth + Tailwind | — | #26 |
|
||||
| P3-002 | not-started | Phase 3 | Auth pages — login, registration, SSO redirect | — | #27 |
|
||||
|
||||
Reference in New Issue
Block a user