feat: Emit account_switched event on account exhaustion in lifecycle controller

Passes EventBus through LifecycleFactory and AgentLifecycleController so that
when an account is marked exhausted, an agent:account_switched event is emitted
with the previous and new account IDs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas May
2026-03-06 13:12:25 +01:00
parent 3f3954672e
commit 3e2a570447
3 changed files with 33 additions and 5 deletions

View File

@@ -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<void> {
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,

View File

@@ -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;

View File

@@ -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(),