diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 9468ebafad..3d0b83aba3 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -7,7 +7,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | Event | Mode | Declared in | Dispatchers | Listeners | | --- | --- | --- | --- | --- | -| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:338`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`stdio-agent`](../packages/ui/stdio-agent) | +| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:338`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | [`stdio-agent`](../packages/ui/stdio-agent) | | `agent/created` | `emit` | [`packages/core/agent/src/types.ts:303`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`stdio-agent`](../packages/ui/stdio-agent) | | `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:318`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`stdio-agent`](../packages/ui/stdio-agent) | | `agent/error` | `emit` | [`packages/core/agent/src/types.ts:592`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - | diff --git a/packages/core/agent-loop/src/index.ts b/packages/core/agent-loop/src/index.ts index 0307ac622a..17c8087df5 100644 --- a/packages/core/agent-loop/src/index.ts +++ b/packages/core/agent-loop/src/index.ts @@ -422,10 +422,16 @@ export class AgentLoop extends Service implements AgentFactory { error: unknown, ): void { this.ctx.logger.warn(`agent "${configId}": config-driven ${action} of "${sessionId}" failed: ${String(error)}`) - try { - this.ctx.emit('agent-loop/config-start-failed', sessionId, error) - } catch (listenerError) { - this.ctx.logger.warn(`agent "${configId}": config-start-failed listener threw: ${String(listenerError)}`) + const args: unknown[] = ['agent-loop/config-start-failed', sessionId, error] + for (const callback of this.ctx.events.dispatch('emit', args)) { + try { + const returned: unknown = callback(...args) + void Promise.resolve(returned).catch((listenerError: unknown) => { + this.ctx.logger.warn(`agent "${configId}": config-start-failed listener rejected: ${String(listenerError)}`) + }) + } catch (listenerError: unknown) { + this.ctx.logger.warn(`agent "${configId}": config-start-failed listener threw: ${String(listenerError)}`) + } } } diff --git a/packages/core/agent-loop/tests/config-session-id.spec.ts b/packages/core/agent-loop/tests/config-session-id.spec.ts index 77f69402ba..daf878e2c2 100644 --- a/packages/core/agent-loop/tests/config-session-id.spec.ts +++ b/packages/core/agent-loop/tests/config-session-id.spec.ts @@ -99,11 +99,13 @@ describe('config-driven session id', () => { await ctx.plugin(SessionPersistenceJsonl, { root }) const failure = new Error('persistence index failed') const listenerFailure = new Error('failure observer failed') + const asyncListenerFailure = new Error('async failure observer failed') const failures: { sessionId: SessionId; error: unknown }[] = [] + ctx.on('agent-loop/config-start-failed', () => { throw listenerFailure }) + ctx.on('agent-loop/config-start-failed', () => Promise.reject(asyncListenerFailure) as never) ctx.on('agent-loop/config-start-failed', (sessionId, error) => { failures.push({ sessionId, error }) }) - ctx.on('agent-loop/config-start-failed', () => { throw listenerFailure }) vi.spyOn(ctx.sessionPersistence, 'list').mockRejectedValue(failure) const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => undefined) @@ -118,6 +120,9 @@ describe('config-driven session id', () => { expect(warn).toHaveBeenCalledWith( 'agent "main": config-start-failed listener threw: Error: failure observer failed', ) + await expect.poll(() => warn).toHaveBeenCalledWith( + 'agent "main": config-start-failed listener rejected: Error: async failure observer failed', + ) expect(ctx.agents.get(SessionId('stdio-exact-failure'))).toBeUndefined() warn.mockRestore() await ctx.fiber.dispose() diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 5144e388a2..fa7daed85e 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -245,6 +245,9 @@ const DYNAMIC_EVENT_DISPATCHERS: Array<{ event: string; pkg: string; method: str // Registry disposal reuses the stable carrier captured before entry commit // and contains each listener directly rather than rebuilding via agentEvents. { event: 'agent/disposed', pkg: 'agent', method: 'events.dispatch' }, + // Config startup failures have no live Agent carrier; AgentLoop resolves the + // callbacks directly to contain each synchronous throw and async rejection. + { event: 'agent-loop/config-start-failed', pkg: 'agent-loop', method: 'events.dispatch' }, { event: 'session/created', pkg: 'session', method: 'events.dispatch' }, // Session event callbacks are likewise resolved before the log push, then // invoked individually after commit so observer failures are contained.