Forráskód Böngészése

fix: contain config failure observers

Tianyi Cui 2 hónapja
szülő
commit
7d6ef0a472

+ 1 - 1
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:339`](../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:339`](../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:304`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`jsonrpc`](../packages/ui/jsonrpc), [`stdio-agent`](../packages/ui/stdio-agent) |
 | `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:319`](../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:593`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - |

+ 10 - 4
packages/core/agent-loop/src/index.ts

@@ -423,10 +423,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)}`)
+      }
     }
   }
 

+ 6 - 1
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()

+ 3 - 0
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.