fix(#338): Add session cleanup on terminal states
- Add removeSession and scheduleSessionCleanup methods to AgentSpawnerService - Schedule session cleanup after completed/failed/killed transitions - Default 30 second delay before cleanup to allow status queries - Implement OnModuleDestroy to clean up pending timers - Add forwardRef injection to avoid circular dependency - Add comprehensive tests for cleanup functionality Refs #338
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Injectable, Logger, HttpException, HttpStatus } from "@nestjs/common";
|
||||
import { Injectable, Logger, HttpException, HttpStatus, OnModuleDestroy } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
import { randomUUID } from "crypto";
|
||||
@@ -9,15 +9,23 @@ import {
|
||||
AgentType,
|
||||
} from "./types/agent-spawner.types";
|
||||
|
||||
/**
|
||||
* Default delay in milliseconds before cleaning up sessions after terminal states
|
||||
* This allows time for status queries before the session is removed
|
||||
*/
|
||||
const DEFAULT_SESSION_CLEANUP_DELAY_MS = 30000; // 30 seconds
|
||||
|
||||
/**
|
||||
* Service responsible for spawning Claude agents using Anthropic SDK
|
||||
*/
|
||||
@Injectable()
|
||||
export class AgentSpawnerService {
|
||||
export class AgentSpawnerService implements OnModuleDestroy {
|
||||
private readonly logger = new Logger(AgentSpawnerService.name);
|
||||
private readonly anthropic: Anthropic;
|
||||
private readonly sessions = new Map<string, AgentSession>();
|
||||
private readonly maxConcurrentAgents: number;
|
||||
private readonly sessionCleanupDelayMs: number;
|
||||
private readonly cleanupTimers = new Map<string, NodeJS.Timeout>();
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
const apiKey = this.configService.get<string>("orchestrator.claude.apiKey");
|
||||
@@ -34,11 +42,27 @@ export class AgentSpawnerService {
|
||||
this.maxConcurrentAgents =
|
||||
this.configService.get<number>("orchestrator.spawner.maxConcurrentAgents") ?? 20;
|
||||
|
||||
// Default to 30 seconds if not configured
|
||||
this.sessionCleanupDelayMs =
|
||||
this.configService.get<number>("orchestrator.spawner.sessionCleanupDelayMs") ??
|
||||
DEFAULT_SESSION_CLEANUP_DELAY_MS;
|
||||
|
||||
this.logger.log(
|
||||
`AgentSpawnerService initialized with Claude SDK (max concurrent agents: ${String(this.maxConcurrentAgents)})`
|
||||
`AgentSpawnerService initialized with Claude SDK (max concurrent agents: ${String(this.maxConcurrentAgents)}, cleanup delay: ${String(this.sessionCleanupDelayMs)}ms)`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all pending cleanup timers on module destroy
|
||||
*/
|
||||
onModuleDestroy(): void {
|
||||
this.cleanupTimers.forEach((timer, agentId) => {
|
||||
clearTimeout(timer);
|
||||
this.logger.debug(`Cleared cleanup timer for agent ${agentId}`);
|
||||
});
|
||||
this.cleanupTimers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a new agent with the given configuration
|
||||
* @param request Agent spawn request
|
||||
@@ -100,6 +124,59 @@ export class AgentSpawnerService {
|
||||
return Array.from(this.sessions.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an agent session from the in-memory map
|
||||
* @param agentId Unique agent identifier
|
||||
* @returns true if session was removed, false if not found
|
||||
*/
|
||||
removeSession(agentId: string): boolean {
|
||||
// Clear any pending cleanup timer for this agent
|
||||
const timer = this.cleanupTimers.get(agentId);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
this.cleanupTimers.delete(agentId);
|
||||
}
|
||||
|
||||
const deleted = this.sessions.delete(agentId);
|
||||
if (deleted) {
|
||||
this.logger.log(`Session removed for agent ${agentId}`);
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule session cleanup after a delay
|
||||
* This allows time for status queries before the session is removed
|
||||
* @param agentId Unique agent identifier
|
||||
* @param delayMs Optional delay in milliseconds (defaults to configured value)
|
||||
*/
|
||||
scheduleSessionCleanup(agentId: string, delayMs?: number): void {
|
||||
const delay = delayMs ?? this.sessionCleanupDelayMs;
|
||||
|
||||
// Clear any existing timer for this agent
|
||||
const existingTimer = this.cleanupTimers.get(agentId);
|
||||
if (existingTimer) {
|
||||
clearTimeout(existingTimer);
|
||||
}
|
||||
|
||||
this.logger.debug(`Scheduling session cleanup for agent ${agentId} in ${String(delay)}ms`);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
this.removeSession(agentId);
|
||||
this.cleanupTimers.delete(agentId);
|
||||
}, delay);
|
||||
|
||||
this.cleanupTimers.set(agentId, timer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of pending cleanup timers (for testing)
|
||||
* @returns Number of pending cleanup timers
|
||||
*/
|
||||
getPendingCleanupCount(): number {
|
||||
return this.cleanupTimers.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the concurrent agent limit has been reached
|
||||
* @throws HttpException with 429 Too Many Requests if limit reached
|
||||
|
||||
Reference in New Issue
Block a user