diff --git a/apps/server/agent/lifecycle/controller.ts b/apps/server/agent/lifecycle/controller.ts index 833634d..537542b 100644 --- a/apps/server/agent/lifecycle/controller.ts +++ b/apps/server/agent/lifecycle/controller.ts @@ -21,6 +21,7 @@ import type { RetryPolicy, AgentError } from './retry-policy.js'; import { AgentExhaustedError, AgentFailureError } from './retry-policy.js'; import type { AgentErrorAnalyzer } from './error-analyzer.js'; import type { CleanupStrategy, AgentInfo } from './cleanup-strategy.js'; +import type { EventBus, AgentAccountSwitchedEvent } from '../../events/types.js'; const log = createModuleLogger('lifecycle-controller'); @@ -48,6 +49,7 @@ export class AgentLifecycleController { private cleanupStrategy: CleanupStrategy, private accountRepository?: AccountRepository, private debug: boolean = false, + private eventBus?: EventBus, ) {} /** @@ -304,7 +306,7 @@ export class AgentLifecycleController { } /** - * Handle account exhaustion by marking account as exhausted. + * Handle account exhaustion by marking account as exhausted and emitting account_switched event. */ private async handleAccountExhaustion(agentId: string): Promise { if (!this.accountRepository) { @@ -319,15 +321,34 @@ export class AgentLifecycleController { return; } + const previousAccountId = agent.accountId; + // Mark account as exhausted for 1 hour const exhaustedUntil = new Date(Date.now() + 60 * 60 * 1000); - await this.accountRepository.markExhausted(agent.accountId, exhaustedUntil); + await this.accountRepository.markExhausted(previousAccountId, exhaustedUntil); log.info({ agentId, - accountId: agent.accountId, + accountId: previousAccountId, exhaustedUntil }, 'marked account as exhausted due to usage limits'); + + // Find the next available account and emit account_switched event + const newAccount = await this.accountRepository.findNextAvailable(agent.provider ?? 'claude'); + if (newAccount && this.eventBus) { + const event: AgentAccountSwitchedEvent = { + type: 'agent:account_switched', + timestamp: new Date(), + payload: { + agentId, + name: agent.name, + previousAccountId, + newAccountId: newAccount.id, + reason: 'account_exhausted', + }, + }; + this.eventBus.emit(event); + } } catch (error) { log.warn({ agentId, diff --git a/apps/server/agent/lifecycle/factory.ts b/apps/server/agent/lifecycle/factory.ts index 4bff87b..51c502a 100644 --- a/apps/server/agent/lifecycle/factory.ts +++ b/apps/server/agent/lifecycle/factory.ts @@ -14,6 +14,7 @@ import type { AgentRepository } from '../../db/repositories/agent-repository.js' import type { AccountRepository } from '../../db/repositories/account-repository.js'; import type { ProcessManager } from '../process-manager.js'; import type { CleanupManager } from '../cleanup-manager.js'; +import type { EventBus } from '../../events/types.js'; export interface LifecycleFactoryOptions { repository: AgentRepository; @@ -21,6 +22,7 @@ export interface LifecycleFactoryOptions { cleanupManager: CleanupManager; accountRepository?: AccountRepository; debug?: boolean; + eventBus?: EventBus; } /** @@ -32,7 +34,8 @@ export function createLifecycleController(options: LifecycleFactoryOptions): Age processManager, cleanupManager, accountRepository, - debug = false + debug = false, + eventBus, } = options; // Create core components @@ -51,7 +54,8 @@ export function createLifecycleController(options: LifecycleFactoryOptions): Age cleanupManager, cleanupStrategy, accountRepository, - debug + debug, + eventBus, ); return lifecycleController; diff --git a/apps/server/agent/manager.ts b/apps/server/agent/manager.ts index ac36b83..ce367d7 100644 --- a/apps/server/agent/manager.ts +++ b/apps/server/agent/manager.ts @@ -98,6 +98,7 @@ export class MultiProviderAgentManager implements AgentManager { cleanupManager: this.cleanupManager, accountRepository, debug, + eventBus, }); // Listen for process crashed events to handle agents specially @@ -607,6 +608,7 @@ export class MultiProviderAgentManager implements AgentManager { this.activeAgents.set(agentId, activeEntry); if (this.eventBus) { + // verified: payload matches AgentResumedEvent shape (agentId, name, taskId, sessionId) const event: AgentResumedEvent = { type: 'agent:resumed', timestamp: new Date(), @@ -796,6 +798,7 @@ export class MultiProviderAgentManager implements AgentManager { log.info({ agentId, pid }, 'resume detached subprocess started'); if (this.eventBus) { + // verified: payload matches AgentResumedEvent shape (agentId, name, taskId, sessionId) const event: AgentResumedEvent = { type: 'agent:resumed', timestamp: new Date(),