@namzu/sdk 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/dist/advisory/context.test.d.ts +16 -0
- package/dist/advisory/context.test.d.ts.map +1 -0
- package/dist/advisory/context.test.js +92 -0
- package/dist/advisory/context.test.js.map +1 -0
- package/dist/advisory/evaluator.test.d.ts +34 -0
- package/dist/advisory/evaluator.test.d.ts.map +1 -0
- package/dist/advisory/evaluator.test.js +172 -0
- package/dist/advisory/evaluator.test.js.map +1 -0
- package/dist/advisory/executor.test.d.ts +35 -0
- package/dist/advisory/executor.test.d.ts.map +1 -0
- package/dist/advisory/executor.test.js +233 -0
- package/dist/advisory/executor.test.js.map +1 -0
- package/dist/advisory/registry.test.d.ts +16 -0
- package/dist/advisory/registry.test.d.ts.map +1 -0
- package/dist/advisory/registry.test.js +62 -0
- package/dist/advisory/registry.test.js.map +1 -0
- package/dist/bridge/a2a/agent-card.test.d.ts +24 -0
- package/dist/bridge/a2a/agent-card.test.d.ts.map +1 -0
- package/dist/bridge/a2a/agent-card.test.js +118 -0
- package/dist/bridge/a2a/agent-card.test.js.map +1 -0
- package/dist/bridge/a2a/mapper.test.d.ts +29 -0
- package/dist/bridge/a2a/mapper.test.d.ts.map +1 -0
- package/dist/bridge/a2a/mapper.test.js +265 -0
- package/dist/bridge/a2a/mapper.test.js.map +1 -0
- package/dist/bridge/a2a/message.test.d.ts +20 -0
- package/dist/bridge/a2a/message.test.d.ts.map +1 -0
- package/dist/bridge/a2a/message.test.js +116 -0
- package/dist/bridge/a2a/message.test.js.map +1 -0
- package/dist/bridge/a2a/task.test.d.ts +29 -0
- package/dist/bridge/a2a/task.test.d.ts.map +1 -0
- package/dist/bridge/a2a/task.test.js +198 -0
- package/dist/bridge/a2a/task.test.js.map +1 -0
- package/dist/bridge/mcp/connector/adapter.test.d.ts +27 -0
- package/dist/bridge/mcp/connector/adapter.test.d.ts.map +1 -0
- package/dist/bridge/mcp/connector/adapter.test.js +203 -0
- package/dist/bridge/mcp/connector/adapter.test.js.map +1 -0
- package/dist/bridge/sse/mapper.test.d.ts +27 -0
- package/dist/bridge/sse/mapper.test.d.ts.map +1 -0
- package/dist/bridge/sse/mapper.test.js +271 -0
- package/dist/bridge/sse/mapper.test.js.map +1 -0
- package/dist/bridge/tools/connector/adapter.d.ts +2 -2
- package/dist/bridge/tools/connector/adapter.test.d.ts +28 -0
- package/dist/bridge/tools/connector/adapter.test.d.ts.map +1 -0
- package/dist/bridge/tools/connector/adapter.test.js +182 -0
- package/dist/bridge/tools/connector/adapter.test.js.map +1 -0
- package/dist/bridge/tools/connector/definitions.test.d.ts +23 -0
- package/dist/bridge/tools/connector/definitions.test.d.ts.map +1 -0
- package/dist/bridge/tools/connector/definitions.test.js +158 -0
- package/dist/bridge/tools/connector/definitions.test.js.map +1 -0
- package/dist/bridge/tools/connector/router.test.d.ts +21 -0
- package/dist/bridge/tools/connector/router.test.d.ts.map +1 -0
- package/dist/bridge/tools/connector/router.test.js +139 -0
- package/dist/bridge/tools/connector/router.test.js.map +1 -0
- package/dist/bus/breaker.test.d.ts +41 -0
- package/dist/bus/breaker.test.d.ts.map +1 -0
- package/dist/bus/breaker.test.js +242 -0
- package/dist/bus/breaker.test.js.map +1 -0
- package/dist/bus/index.d.ts +3 -1
- package/dist/bus/index.d.ts.map +1 -1
- package/dist/bus/index.js +18 -11
- package/dist/bus/index.js.map +1 -1
- package/dist/bus/index.test.d.ts +25 -0
- package/dist/bus/index.test.d.ts.map +1 -0
- package/dist/bus/index.test.js +151 -0
- package/dist/bus/index.test.js.map +1 -0
- package/dist/bus/lock.test.d.ts +44 -0
- package/dist/bus/lock.test.d.ts.map +1 -0
- package/dist/bus/lock.test.js +226 -0
- package/dist/bus/lock.test.js.map +1 -0
- package/dist/bus/ownership.test.d.ts +26 -0
- package/dist/bus/ownership.test.d.ts.map +1 -0
- package/dist/bus/ownership.test.js +205 -0
- package/dist/bus/ownership.test.js.map +1 -0
- package/dist/config/runtime.d.ts +28 -28
- package/dist/connector/BaseConnector.test.d.ts +21 -0
- package/dist/connector/BaseConnector.test.d.ts.map +1 -0
- package/dist/connector/BaseConnector.test.js +108 -0
- package/dist/connector/BaseConnector.test.js.map +1 -0
- package/dist/connector/builtins/http.test.d.ts +30 -0
- package/dist/connector/builtins/http.test.d.ts.map +1 -0
- package/dist/connector/builtins/http.test.js +232 -0
- package/dist/connector/builtins/http.test.js.map +1 -0
- package/dist/connector/builtins/webhook.test.d.ts +20 -0
- package/dist/connector/builtins/webhook.test.d.ts.map +1 -0
- package/dist/connector/builtins/webhook.test.js +113 -0
- package/dist/connector/builtins/webhook.test.js.map +1 -0
- package/dist/connector/execution/factory.test.d.ts +16 -0
- package/dist/connector/execution/factory.test.d.ts.map +1 -0
- package/dist/connector/execution/factory.test.js +64 -0
- package/dist/connector/execution/factory.test.js.map +1 -0
- package/dist/connector/execution/remote.test.d.ts +16 -0
- package/dist/connector/execution/remote.test.d.ts.map +1 -0
- package/dist/connector/execution/remote.test.js +53 -0
- package/dist/connector/execution/remote.test.js.map +1 -0
- package/dist/connector/mcp/adapter.test.d.ts +34 -0
- package/dist/connector/mcp/adapter.test.d.ts.map +1 -0
- package/dist/connector/mcp/adapter.test.js +199 -0
- package/dist/connector/mcp/adapter.test.js.map +1 -0
- package/dist/probe/context.d.ts +8 -0
- package/dist/probe/context.d.ts.map +1 -0
- package/dist/probe/context.js +7 -0
- package/dist/probe/context.js.map +1 -0
- package/dist/probe/errors.d.ts +12 -0
- package/dist/probe/errors.d.ts.map +1 -0
- package/dist/probe/errors.js +21 -0
- package/dist/probe/errors.js.map +1 -0
- package/dist/probe/index.d.ts +5 -0
- package/dist/probe/index.d.ts.map +1 -0
- package/dist/probe/index.js +4 -0
- package/dist/probe/index.js.map +1 -0
- package/dist/probe/registry.d.ts +24 -0
- package/dist/probe/registry.d.ts.map +1 -0
- package/dist/probe/registry.js +228 -0
- package/dist/probe/registry.js.map +1 -0
- package/dist/probe/registry.test.d.ts +7 -0
- package/dist/probe/registry.test.d.ts.map +1 -0
- package/dist/probe/registry.test.js +310 -0
- package/dist/probe/registry.test.js.map +1 -0
- package/dist/provider/instrumentation.d.ts +9 -0
- package/dist/provider/instrumentation.d.ts.map +1 -0
- package/dist/provider/instrumentation.js +104 -0
- package/dist/provider/instrumentation.js.map +1 -0
- package/dist/provider/instrumentation.test.d.ts +2 -0
- package/dist/provider/instrumentation.test.d.ts.map +1 -0
- package/dist/provider/instrumentation.test.js +152 -0
- package/dist/provider/instrumentation.test.js.map +1 -0
- package/dist/public-runtime.d.ts +5 -0
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +4 -0
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +3 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/rag/chunking.test.d.ts +20 -0
- package/dist/rag/chunking.test.d.ts.map +1 -0
- package/dist/rag/chunking.test.js +92 -0
- package/dist/rag/chunking.test.js.map +1 -0
- package/dist/rag/context-assembler.test.d.ts +19 -0
- package/dist/rag/context-assembler.test.d.ts.map +1 -0
- package/dist/rag/context-assembler.test.js +98 -0
- package/dist/rag/context-assembler.test.js.map +1 -0
- package/dist/rag/embedding.test.d.ts +19 -0
- package/dist/rag/embedding.test.d.ts.map +1 -0
- package/dist/rag/embedding.test.js +115 -0
- package/dist/rag/embedding.test.js.map +1 -0
- package/dist/rag/ingestion.test.d.ts +22 -0
- package/dist/rag/ingestion.test.d.ts.map +1 -0
- package/dist/rag/ingestion.test.js +99 -0
- package/dist/rag/ingestion.test.js.map +1 -0
- package/dist/rag/knowledge-base.test.d.ts +17 -0
- package/dist/rag/knowledge-base.test.d.ts.map +1 -0
- package/dist/rag/knowledge-base.test.js +77 -0
- package/dist/rag/knowledge-base.test.js.map +1 -0
- package/dist/rag/rag-tool.test.d.ts +21 -0
- package/dist/rag/rag-tool.test.d.ts.map +1 -0
- package/dist/rag/rag-tool.test.js +149 -0
- package/dist/rag/rag-tool.test.js.map +1 -0
- package/dist/rag/retriever.test.d.ts +26 -0
- package/dist/rag/retriever.test.d.ts.map +1 -0
- package/dist/rag/retriever.test.js +180 -0
- package/dist/rag/retriever.test.js.map +1 -0
- package/dist/rag/vector-store.test.d.ts +38 -0
- package/dist/rag/vector-store.test.d.ts.map +1 -0
- package/dist/rag/vector-store.test.js +175 -0
- package/dist/rag/vector-store.test.js.map +1 -0
- package/dist/registry/ManagedRegistry.test.d.ts +21 -0
- package/dist/registry/ManagedRegistry.test.d.ts.map +1 -0
- package/dist/registry/ManagedRegistry.test.js +98 -0
- package/dist/registry/ManagedRegistry.test.js.map +1 -0
- package/dist/registry/Registry.test.d.ts +18 -0
- package/dist/registry/Registry.test.d.ts.map +1 -0
- package/dist/registry/Registry.test.js +79 -0
- package/dist/registry/Registry.test.js.map +1 -0
- package/dist/registry/agent/definitions.test.d.ts +15 -0
- package/dist/registry/agent/definitions.test.d.ts.map +1 -0
- package/dist/registry/agent/definitions.test.js +84 -0
- package/dist/registry/agent/definitions.test.js.map +1 -0
- package/dist/registry/connector/definitions.test.d.ts +13 -0
- package/dist/registry/connector/definitions.test.d.ts.map +1 -0
- package/dist/registry/connector/definitions.test.js +41 -0
- package/dist/registry/connector/definitions.test.js.map +1 -0
- package/dist/registry/connector/scoped.test.d.ts +21 -0
- package/dist/registry/connector/scoped.test.d.ts.map +1 -0
- package/dist/registry/connector/scoped.test.js +115 -0
- package/dist/registry/connector/scoped.test.js.map +1 -0
- package/dist/registry/plugin/index.test.d.ts +12 -0
- package/dist/registry/plugin/index.test.d.ts.map +1 -0
- package/dist/registry/plugin/index.test.js +69 -0
- package/dist/registry/plugin/index.test.js.map +1 -0
- package/dist/registry/tool/execute.test.d.ts +42 -0
- package/dist/registry/tool/execute.test.d.ts.map +1 -0
- package/dist/registry/tool/execute.test.js +281 -0
- package/dist/registry/tool/execute.test.js.map +1 -0
- package/dist/runtime/query/events.d.ts +3 -1
- package/dist/runtime/query/events.d.ts.map +1 -1
- package/dist/runtime/query/events.js +6 -1
- package/dist/runtime/query/events.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +3 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +30 -1
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/iteration/phases/advisory.test.d.ts +42 -0
- package/dist/runtime/query/iteration/phases/advisory.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/advisory.test.js +334 -0
- package/dist/runtime/query/iteration/phases/advisory.test.js.map +1 -0
- package/dist/test-setup.d.ts +22 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +23 -0
- package/dist/test-setup.js.map +1 -0
- package/dist/types/bus/index.d.ts +46 -2
- package/dist/types/bus/index.d.ts.map +1 -1
- package/dist/types/doctor/check.d.ts +41 -0
- package/dist/types/doctor/check.d.ts.map +1 -0
- package/dist/types/doctor/check.js +2 -0
- package/dist/types/doctor/check.js.map +1 -0
- package/dist/types/doctor/index.d.ts +2 -0
- package/dist/types/doctor/index.d.ts.map +1 -0
- package/dist/types/doctor/index.js +2 -0
- package/dist/types/doctor/index.js.map +1 -0
- package/dist/types/probe/event-kind.d.ts +6 -0
- package/dist/types/probe/event-kind.d.ts.map +1 -0
- package/dist/types/probe/event-kind.js +2 -0
- package/dist/types/probe/event-kind.js.map +1 -0
- package/dist/types/probe/event-of.d.ts +5 -0
- package/dist/types/probe/event-of.d.ts.map +1 -0
- package/dist/types/probe/event-of.js +2 -0
- package/dist/types/probe/event-of.js.map +1 -0
- package/dist/types/probe/index.d.ts +4 -0
- package/dist/types/probe/index.d.ts.map +1 -0
- package/dist/types/probe/index.js +2 -0
- package/dist/types/probe/index.js.map +1 -0
- package/dist/types/probe/registry.d.ts +27 -0
- package/dist/types/probe/registry.d.ts.map +1 -0
- package/dist/types/probe/registry.js +2 -0
- package/dist/types/probe/registry.js.map +1 -0
- package/dist/utils/logger.d.ts +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +5 -0
- package/dist/utils/logger.js.map +1 -1
- package/dist/vault/instrumentation.d.ts +11 -0
- package/dist/vault/instrumentation.d.ts.map +1 -0
- package/dist/vault/instrumentation.js +32 -0
- package/dist/vault/instrumentation.js.map +1 -0
- package/dist/vault/instrumentation.test.d.ts +2 -0
- package/dist/vault/instrumentation.test.d.ts.map +1 -0
- package/dist/vault/instrumentation.test.js +80 -0
- package/dist/vault/instrumentation.test.js.map +1 -0
- package/package.json +4 -1
- package/src/advisory/context.test.ts +109 -0
- package/src/advisory/evaluator.test.ts +192 -0
- package/src/advisory/executor.test.ts +272 -0
- package/src/advisory/registry.test.ts +75 -0
- package/src/bridge/a2a/agent-card.test.ts +140 -0
- package/src/bridge/a2a/mapper.test.ts +293 -0
- package/src/bridge/a2a/message.test.ts +138 -0
- package/src/bridge/a2a/task.test.ts +235 -0
- package/src/bridge/mcp/connector/adapter.test.ts +230 -0
- package/src/bridge/sse/mapper.test.ts +422 -0
- package/src/bridge/tools/connector/adapter.test.ts +224 -0
- package/src/bridge/tools/connector/definitions.test.ts +183 -0
- package/src/bridge/tools/connector/router.test.ts +159 -0
- package/src/bus/breaker.test.ts +274 -0
- package/src/bus/index.test.ts +183 -0
- package/src/bus/index.ts +21 -10
- package/src/bus/lock.test.ts +265 -0
- package/src/bus/ownership.test.ts +243 -0
- package/src/connector/BaseConnector.test.ts +130 -0
- package/src/connector/builtins/http.test.ts +290 -0
- package/src/connector/builtins/webhook.test.ts +138 -0
- package/src/connector/execution/factory.test.ts +75 -0
- package/src/connector/execution/remote.test.ts +63 -0
- package/src/connector/mcp/adapter.test.ts +249 -0
- package/src/probe/context.ts +14 -0
- package/src/probe/errors.ts +27 -0
- package/src/probe/index.ts +4 -0
- package/src/probe/registry.test.ts +480 -0
- package/src/probe/registry.ts +276 -0
- package/src/provider/instrumentation.test.ts +192 -0
- package/src/provider/instrumentation.ts +139 -0
- package/src/public-runtime.ts +17 -0
- package/src/public-types.ts +3 -0
- package/src/rag/chunking.test.ts +107 -0
- package/src/rag/context-assembler.test.ts +114 -0
- package/src/rag/embedding.test.ts +130 -0
- package/src/rag/ingestion.test.ts +114 -0
- package/src/rag/knowledge-base.test.ts +106 -0
- package/src/rag/rag-tool.test.ts +167 -0
- package/src/rag/retriever.test.ts +210 -0
- package/src/rag/vector-store.test.ts +196 -0
- package/src/registry/ManagedRegistry.test.ts +118 -0
- package/src/registry/Registry.test.ts +91 -0
- package/src/registry/agent/definitions.test.ts +100 -0
- package/src/registry/connector/definitions.test.ts +51 -0
- package/src/registry/connector/scoped.test.ts +129 -0
- package/src/registry/plugin/index.test.ts +85 -0
- package/src/registry/tool/execute.test.ts +330 -0
- package/src/runtime/query/events.ts +6 -1
- package/src/runtime/query/executor.ts +34 -0
- package/src/runtime/query/iteration/phases/advisory.test.ts +412 -0
- package/src/test-setup.ts +24 -0
- package/src/types/bus/index.ts +54 -2
- package/src/types/doctor/check.ts +53 -0
- package/src/types/doctor/index.ts +9 -0
- package/src/types/probe/event-kind.ts +8 -0
- package/src/types/probe/event-of.ts +3 -0
- package/src/types/probe/index.ts +11 -0
- package/src/types/probe/registry.ts +36 -0
- package/src/utils/logger.ts +6 -1
- package/src/vault/instrumentation.test.ts +98 -0
- package/src/vault/instrumentation.ts +56 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Current-code invariants asserted (2026-04-21, ses_006 Phase 1):
|
|
3
|
+
*
|
|
4
|
+
* - `canExecute(runId)` on an unknown runId returns true (no breaker
|
|
5
|
+
* means no constraint).
|
|
6
|
+
* - `recordFailure(runId)` lazily creates a breaker entry in `closed`
|
|
7
|
+
* state for unknown runIds; `recordSuccess(runId)` is a no-op when
|
|
8
|
+
* no entry exists.
|
|
9
|
+
* - Consecutive failures: after `failureThreshold` calls in a row
|
|
10
|
+
* without intervening success, `state` → `open`, `trippedAt` set,
|
|
11
|
+
* single `breaker_tripped` event.
|
|
12
|
+
* - In `open` state, `canExecute` returns false until `resetTimeoutMs`
|
|
13
|
+
* has elapsed since `trippedAt`; at that point it transitions to
|
|
14
|
+
* `half_open`, emits `breaker_half_open`, and returns true.
|
|
15
|
+
* - In `half_open`, `canExecute` keeps returning true (the probe is
|
|
16
|
+
* allowed more than once) until the next `recordSuccess` closes the
|
|
17
|
+
* breaker or the next `recordFailure` re-trips it.
|
|
18
|
+
* - `recordSuccess` in `half_open` → `closed`; emits
|
|
19
|
+
* `breaker_probe_success` then `breaker_reset` (two events, in
|
|
20
|
+
* that order).
|
|
21
|
+
* - `recordFailure` in `half_open` → `open`; emits
|
|
22
|
+
* `breaker_probe_failure` then `breaker_tripped` (two events).
|
|
23
|
+
* The `consecutiveFailures` count carries over (not reset by the
|
|
24
|
+
* half-open probe cycle).
|
|
25
|
+
* - `recordSuccess` in `closed` resets `consecutiveFailures` to 0 and
|
|
26
|
+
* updates `lastSuccessAt`; emits nothing.
|
|
27
|
+
* - `recordSuccess` while `open`: logs a warning, does NOT change
|
|
28
|
+
* state (behaviour is "discarded"); emits nothing.
|
|
29
|
+
* - `recordFailure` while `open`: no additional events; state stays
|
|
30
|
+
* open (consecutive counter does NOT advance from `recordFailure`
|
|
31
|
+
* while open in the current implementation either — see test).
|
|
32
|
+
* - `reset(runId)` forces the breaker to `closed` regardless of prior
|
|
33
|
+
* state; emits `breaker_reset`; clears `consecutiveFailures` and
|
|
34
|
+
* `trippedAt` but preserves `lastFailureAt` / `lastSuccessAt`.
|
|
35
|
+
* - `listTripped()` returns snapshots for every breaker in `open` or
|
|
36
|
+
* `half_open`; closed breakers are excluded.
|
|
37
|
+
* - Breaker entries are keyed per-`RunId`; state does not leak across
|
|
38
|
+
* runs. No per-tenant dimension (design.md §2.1 aspirational).
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import fc from 'fast-check'
|
|
42
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
43
|
+
|
|
44
|
+
import type { AgentBusEvent } from '../types/bus/index.js'
|
|
45
|
+
import type { RunId } from '../types/ids/index.js'
|
|
46
|
+
import type { Logger } from '../utils/logger.js'
|
|
47
|
+
|
|
48
|
+
import { CircuitBreaker } from './breaker.js'
|
|
49
|
+
|
|
50
|
+
function makeLogger(): Logger {
|
|
51
|
+
const stub = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }
|
|
52
|
+
return { ...stub, child: vi.fn(() => ({ ...stub, child: vi.fn() })) } as unknown as Logger
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function runId(n: number): RunId {
|
|
56
|
+
return `run_${n}` as RunId
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const THRESHOLD = 5
|
|
60
|
+
const RESET_MS = 30_000
|
|
61
|
+
|
|
62
|
+
describe('CircuitBreaker', () => {
|
|
63
|
+
let events: AgentBusEvent[]
|
|
64
|
+
let breaker: CircuitBreaker
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
events = []
|
|
68
|
+
breaker = new CircuitBreaker(makeLogger(), (e) => events.push(e), THRESHOLD, RESET_MS)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
vi.useRealTimers()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('canExecute', () => {
|
|
76
|
+
it('returns true for an unknown runId (no breaker entry yet)', () => {
|
|
77
|
+
expect(breaker.canExecute(runId(1))).toBe(true)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('recordFailure', () => {
|
|
82
|
+
it('lazily creates a closed breaker entry for unknown runIds', () => {
|
|
83
|
+
breaker.recordFailure(runId(1))
|
|
84
|
+
const snap = breaker.getSnapshot(runId(1))
|
|
85
|
+
expect(snap?.state).toBe('closed')
|
|
86
|
+
expect(snap?.consecutiveFailures).toBe(1)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('trips after exactly `failureThreshold` consecutive failures', () => {
|
|
90
|
+
for (let i = 0; i < THRESHOLD - 1; i++) {
|
|
91
|
+
breaker.recordFailure(runId(1))
|
|
92
|
+
}
|
|
93
|
+
expect(breaker.getSnapshot(runId(1))?.state).toBe('closed')
|
|
94
|
+
expect(events.filter((e) => e.type === 'breaker_tripped')).toHaveLength(0)
|
|
95
|
+
|
|
96
|
+
breaker.recordFailure(runId(1))
|
|
97
|
+
expect(breaker.getSnapshot(runId(1))?.state).toBe('open')
|
|
98
|
+
const trippedEvents = events.filter((e) => e.type === 'breaker_tripped')
|
|
99
|
+
expect(trippedEvents).toHaveLength(1)
|
|
100
|
+
if (trippedEvents[0]?.type === 'breaker_tripped') {
|
|
101
|
+
expect(trippedEvents[0].consecutiveFailures).toBe(THRESHOLD)
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('trips exactly once — further failures in `open` emit no new breaker_tripped', () => {
|
|
106
|
+
for (let i = 0; i < THRESHOLD; i++) breaker.recordFailure(runId(1))
|
|
107
|
+
events.length = 0
|
|
108
|
+
|
|
109
|
+
breaker.recordFailure(runId(1))
|
|
110
|
+
breaker.recordFailure(runId(1))
|
|
111
|
+
expect(events).toEqual([])
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('property: any N ≥ threshold consecutive failures trips exactly once', () => {
|
|
115
|
+
fc.assert(
|
|
116
|
+
fc.property(fc.integer({ min: THRESHOLD, max: THRESHOLD * 4 }), (n) => {
|
|
117
|
+
const local = new CircuitBreaker(makeLogger(), () => {}, THRESHOLD, RESET_MS)
|
|
118
|
+
for (let i = 0; i < n; i++) local.recordFailure(runId(999))
|
|
119
|
+
expect(local.getSnapshot(runId(999))?.state).toBe('open')
|
|
120
|
+
}),
|
|
121
|
+
{ numRuns: 25 },
|
|
122
|
+
)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('property: any N < threshold keeps the breaker closed', () => {
|
|
126
|
+
fc.assert(
|
|
127
|
+
fc.property(fc.integer({ min: 0, max: THRESHOLD - 1 }), (n) => {
|
|
128
|
+
const local = new CircuitBreaker(makeLogger(), () => {}, THRESHOLD, RESET_MS)
|
|
129
|
+
for (let i = 0; i < n; i++) local.recordFailure(runId(888))
|
|
130
|
+
const snap = local.getSnapshot(runId(888))
|
|
131
|
+
if (n === 0) {
|
|
132
|
+
expect(snap).toBeUndefined()
|
|
133
|
+
} else {
|
|
134
|
+
expect(snap?.state).toBe('closed')
|
|
135
|
+
}
|
|
136
|
+
}),
|
|
137
|
+
{ numRuns: 25 },
|
|
138
|
+
)
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe('recordSuccess', () => {
|
|
143
|
+
it('is a no-op on an unknown runId — no breaker entry created', () => {
|
|
144
|
+
breaker.recordSuccess(runId(42))
|
|
145
|
+
expect(breaker.getSnapshot(runId(42))).toBeUndefined()
|
|
146
|
+
expect(events).toEqual([])
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('resets consecutiveFailures to 0 without changing closed state', () => {
|
|
150
|
+
breaker.recordFailure(runId(1))
|
|
151
|
+
breaker.recordFailure(runId(1))
|
|
152
|
+
breaker.recordSuccess(runId(1))
|
|
153
|
+
const snap = breaker.getSnapshot(runId(1))
|
|
154
|
+
expect(snap?.state).toBe('closed')
|
|
155
|
+
expect(snap?.consecutiveFailures).toBe(0)
|
|
156
|
+
expect(snap?.lastSuccessAt).toBeDefined()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('is discarded (warned, no state change) when called while breaker is open', () => {
|
|
160
|
+
for (let i = 0; i < THRESHOLD; i++) breaker.recordFailure(runId(1))
|
|
161
|
+
events.length = 0
|
|
162
|
+
|
|
163
|
+
breaker.recordSuccess(runId(1))
|
|
164
|
+
expect(breaker.getSnapshot(runId(1))?.state).toBe('open')
|
|
165
|
+
expect(events).toEqual([])
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
describe('open → half_open transition', () => {
|
|
170
|
+
it('transitions to half_open after resetTimeoutMs elapsed; emits breaker_half_open', () => {
|
|
171
|
+
vi.useFakeTimers()
|
|
172
|
+
const trip = new CircuitBreaker(makeLogger(), (e) => events.push(e), THRESHOLD, RESET_MS)
|
|
173
|
+
for (let i = 0; i < THRESHOLD; i++) trip.recordFailure(runId(1))
|
|
174
|
+
events.length = 0
|
|
175
|
+
|
|
176
|
+
expect(trip.canExecute(runId(1))).toBe(false)
|
|
177
|
+
|
|
178
|
+
vi.advanceTimersByTime(RESET_MS - 1)
|
|
179
|
+
expect(trip.canExecute(runId(1))).toBe(false)
|
|
180
|
+
|
|
181
|
+
vi.advanceTimersByTime(1)
|
|
182
|
+
expect(trip.canExecute(runId(1))).toBe(true)
|
|
183
|
+
expect(trip.getSnapshot(runId(1))?.state).toBe('half_open')
|
|
184
|
+
expect(events).toEqual([{ type: 'breaker_half_open', agentRunId: runId(1) }])
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('canExecute in half_open keeps returning true until success or failure resolves', () => {
|
|
188
|
+
vi.useFakeTimers()
|
|
189
|
+
const trip = new CircuitBreaker(makeLogger(), () => {}, THRESHOLD, RESET_MS)
|
|
190
|
+
for (let i = 0; i < THRESHOLD; i++) trip.recordFailure(runId(1))
|
|
191
|
+
vi.advanceTimersByTime(RESET_MS)
|
|
192
|
+
trip.canExecute(runId(1)) // flip to half_open
|
|
193
|
+
expect(trip.canExecute(runId(1))).toBe(true)
|
|
194
|
+
expect(trip.canExecute(runId(1))).toBe(true)
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
describe('half_open → closed / open resolution', () => {
|
|
199
|
+
function setupHalfOpen(): CircuitBreaker {
|
|
200
|
+
vi.useFakeTimers()
|
|
201
|
+
const b = new CircuitBreaker(makeLogger(), (e) => events.push(e), THRESHOLD, RESET_MS)
|
|
202
|
+
for (let i = 0; i < THRESHOLD; i++) b.recordFailure(runId(1))
|
|
203
|
+
vi.advanceTimersByTime(RESET_MS)
|
|
204
|
+
b.canExecute(runId(1)) // flip
|
|
205
|
+
events.length = 0
|
|
206
|
+
return b
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
it('recordSuccess in half_open closes the breaker + emits probe_success then reset', () => {
|
|
210
|
+
const b = setupHalfOpen()
|
|
211
|
+
b.recordSuccess(runId(1))
|
|
212
|
+
expect(b.getSnapshot(runId(1))?.state).toBe('closed')
|
|
213
|
+
expect(b.getSnapshot(runId(1))?.consecutiveFailures).toBe(0)
|
|
214
|
+
expect(events).toEqual([
|
|
215
|
+
{ type: 'breaker_probe_success', agentRunId: runId(1) },
|
|
216
|
+
{ type: 'breaker_reset', agentRunId: runId(1) },
|
|
217
|
+
])
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('recordFailure in half_open re-trips + emits probe_failure then tripped', () => {
|
|
221
|
+
const b = setupHalfOpen()
|
|
222
|
+
b.recordFailure(runId(1))
|
|
223
|
+
expect(b.getSnapshot(runId(1))?.state).toBe('open')
|
|
224
|
+
expect(events).toEqual([
|
|
225
|
+
{ type: 'breaker_probe_failure', agentRunId: runId(1) },
|
|
226
|
+
{
|
|
227
|
+
type: 'breaker_tripped',
|
|
228
|
+
agentRunId: runId(1),
|
|
229
|
+
consecutiveFailures: THRESHOLD + 1,
|
|
230
|
+
},
|
|
231
|
+
])
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
describe('reset', () => {
|
|
236
|
+
it('forces a tripped breaker back to closed + emits breaker_reset', () => {
|
|
237
|
+
for (let i = 0; i < THRESHOLD; i++) breaker.recordFailure(runId(1))
|
|
238
|
+
events.length = 0
|
|
239
|
+
|
|
240
|
+
breaker.reset(runId(1))
|
|
241
|
+
const snap = breaker.getSnapshot(runId(1))
|
|
242
|
+
expect(snap?.state).toBe('closed')
|
|
243
|
+
expect(snap?.consecutiveFailures).toBe(0)
|
|
244
|
+
expect(snap?.trippedAt).toBeUndefined()
|
|
245
|
+
expect(events).toEqual([{ type: 'breaker_reset', agentRunId: runId(1) }])
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('is a no-op on an unknown runId (no event)', () => {
|
|
249
|
+
breaker.reset(runId(999))
|
|
250
|
+
expect(events).toEqual([])
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
describe('per-runId isolation', () => {
|
|
255
|
+
it('tripping one runId does not affect another', () => {
|
|
256
|
+
for (let i = 0; i < THRESHOLD; i++) breaker.recordFailure(runId(1))
|
|
257
|
+
expect(breaker.getSnapshot(runId(1))?.state).toBe('open')
|
|
258
|
+
expect(breaker.canExecute(runId(2))).toBe(true)
|
|
259
|
+
expect(breaker.getSnapshot(runId(2))).toBeUndefined()
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
describe('listTripped', () => {
|
|
264
|
+
it('returns only open + half_open breakers; excludes closed', () => {
|
|
265
|
+
for (let i = 0; i < THRESHOLD; i++) breaker.recordFailure(runId(1))
|
|
266
|
+
breaker.recordFailure(runId(2))
|
|
267
|
+
|
|
268
|
+
const tripped = breaker.listTripped()
|
|
269
|
+
expect(tripped).toHaveLength(1)
|
|
270
|
+
expect(tripped[0]?.agentRunId).toBe(runId(1))
|
|
271
|
+
expect(tripped[0]?.state).toBe('open')
|
|
272
|
+
})
|
|
273
|
+
})
|
|
274
|
+
})
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Current-code invariants asserted (2026-04-21, ses_006 Phase 1):
|
|
3
|
+
*
|
|
4
|
+
* - `AgentBus` composes a `FileLockManager`, `EditOwnershipTracker`,
|
|
5
|
+
* and `CircuitBreaker`; every event each of them emits is
|
|
6
|
+
* broadcast to bus-level listeners.
|
|
7
|
+
* - `on(listener)` returns an unsubscribe function; invoking it
|
|
8
|
+
* removes the listener, and subsequent events skip it.
|
|
9
|
+
* - Listeners receive events in the order they are emitted
|
|
10
|
+
* (preserved by `Set` insertion-order iteration). Multi-listener
|
|
11
|
+
* fan-out: every listener sees every event.
|
|
12
|
+
* - A throwing listener does NOT cascade — other listeners still
|
|
13
|
+
* receive the same event, and the bus logs the error via
|
|
14
|
+
* `log.error`.
|
|
15
|
+
* - No per-tenant routing — events are global to the bus instance
|
|
16
|
+
* (design.md §2.1 aspirational per-tenant ordering does not
|
|
17
|
+
* exist; see §2.7).
|
|
18
|
+
* - `cleanupAgent(runId)` releases every lock + every ownership +
|
|
19
|
+
* resets the breaker for that runId. Counts are logged; the
|
|
20
|
+
* method does not return them.
|
|
21
|
+
* - `maintenance()` expires stale locks via `locks.expireStale()`
|
|
22
|
+
* and logs the count when non-zero.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
26
|
+
|
|
27
|
+
import type { AgentBusEvent } from '../types/bus/index.js'
|
|
28
|
+
import type { RunId } from '../types/ids/index.js'
|
|
29
|
+
import type { Logger } from '../utils/logger.js'
|
|
30
|
+
|
|
31
|
+
import { AgentBus } from './index.js'
|
|
32
|
+
|
|
33
|
+
function makeLogger(): Logger {
|
|
34
|
+
// Recursive self-returning child — AgentBus nests FileLockManager /
|
|
35
|
+
// EditOwnershipTracker / CircuitBreaker, each of which chains a
|
|
36
|
+
// `log.child(...)` call in its constructor.
|
|
37
|
+
const self = {
|
|
38
|
+
info: vi.fn(),
|
|
39
|
+
warn: vi.fn(),
|
|
40
|
+
error: vi.fn(),
|
|
41
|
+
debug: vi.fn(),
|
|
42
|
+
child: vi.fn(),
|
|
43
|
+
} as unknown as Logger
|
|
44
|
+
;(self as { child: (ctx: unknown) => Logger }).child = vi.fn(() => self)
|
|
45
|
+
return self
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function runId(n: number): RunId {
|
|
49
|
+
return `run_${n}` as RunId
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('AgentBus', () => {
|
|
53
|
+
let bus: AgentBus
|
|
54
|
+
let log: Logger
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
log = makeLogger()
|
|
58
|
+
bus = new AgentBus(log, { lockTimeoutMs: 60_000, lockAcquireTimeoutMs: 60 })
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
vi.useRealTimers()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('listener lifecycle', () => {
|
|
66
|
+
it('broadcasts events to every registered listener', async () => {
|
|
67
|
+
const a: AgentBusEvent[] = []
|
|
68
|
+
const b: AgentBusEvent[] = []
|
|
69
|
+
bus.on((e) => a.push(e))
|
|
70
|
+
bus.on((e) => b.push(e))
|
|
71
|
+
|
|
72
|
+
await bus.locks.acquire('/tmp/f.txt', runId(1))
|
|
73
|
+
|
|
74
|
+
expect(a.length).toBeGreaterThan(0)
|
|
75
|
+
expect(b).toEqual(a)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('unsubscribe removes the listener; later events skip it', async () => {
|
|
79
|
+
const seen: AgentBusEvent[] = []
|
|
80
|
+
const off = bus.on((e) => seen.push(e))
|
|
81
|
+
|
|
82
|
+
await bus.locks.acquire('/tmp/a.txt', runId(1))
|
|
83
|
+
const countAfterFirst = seen.length
|
|
84
|
+
expect(countAfterFirst).toBeGreaterThan(0)
|
|
85
|
+
|
|
86
|
+
off()
|
|
87
|
+
await bus.locks.acquire('/tmp/b.txt', runId(1))
|
|
88
|
+
expect(seen.length).toBe(countAfterFirst)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('preserves emission order per listener (Set insertion-order iteration)', async () => {
|
|
92
|
+
const seen: string[] = []
|
|
93
|
+
bus.on((e) => seen.push(e.type))
|
|
94
|
+
|
|
95
|
+
await bus.locks.acquire('/tmp/a.txt', runId(1))
|
|
96
|
+
bus.ownership.claim('/tmp/a.txt', runId(1))
|
|
97
|
+
bus.ownership.release('/tmp/a.txt', runId(1))
|
|
98
|
+
bus.locks.release('/tmp/a.txt', runId(1))
|
|
99
|
+
|
|
100
|
+
expect(seen).toEqual([
|
|
101
|
+
'lock_acquired',
|
|
102
|
+
'ownership_claimed',
|
|
103
|
+
'ownership_released',
|
|
104
|
+
'lock_released',
|
|
105
|
+
])
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('a throwing listener does not cascade — other listeners still fire', async () => {
|
|
109
|
+
const good: AgentBusEvent[] = []
|
|
110
|
+
bus.on(() => {
|
|
111
|
+
throw new Error('boom')
|
|
112
|
+
})
|
|
113
|
+
bus.on((e) => good.push(e))
|
|
114
|
+
|
|
115
|
+
await bus.locks.acquire('/tmp/a.txt', runId(1))
|
|
116
|
+
expect(good.length).toBeGreaterThan(0)
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('composed sub-components', () => {
|
|
121
|
+
it('CircuitBreaker events flow through the bus listener', () => {
|
|
122
|
+
const seen: AgentBusEvent[] = []
|
|
123
|
+
bus.on((e) => seen.push(e))
|
|
124
|
+
|
|
125
|
+
for (let i = 0; i < 5; i++) bus.breaker.recordFailure(runId(1))
|
|
126
|
+
expect(seen.filter((e) => e.type === 'breaker_tripped')).toHaveLength(1)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('EditOwnershipTracker events flow through the bus listener', () => {
|
|
130
|
+
const seen: AgentBusEvent[] = []
|
|
131
|
+
bus.on((e) => seen.push(e))
|
|
132
|
+
|
|
133
|
+
bus.ownership.claim('/tmp/a.txt', runId(1))
|
|
134
|
+
bus.ownership.transfer('/tmp/a.txt', runId(1), runId(2))
|
|
135
|
+
bus.ownership.release('/tmp/a.txt', runId(2))
|
|
136
|
+
|
|
137
|
+
expect(seen.map((e) => e.type)).toEqual([
|
|
138
|
+
'ownership_claimed',
|
|
139
|
+
'ownership_transferred',
|
|
140
|
+
'ownership_released',
|
|
141
|
+
])
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('cleanupAgent', () => {
|
|
146
|
+
it('releases locks + ownerships + resets breaker for the runId', async () => {
|
|
147
|
+
await bus.locks.acquire('/tmp/a.txt', runId(1))
|
|
148
|
+
bus.ownership.claim('/tmp/a.txt', runId(1))
|
|
149
|
+
for (let i = 0; i < 5; i++) bus.breaker.recordFailure(runId(1))
|
|
150
|
+
expect(bus.breaker.getSnapshot(runId(1))?.state).toBe('open')
|
|
151
|
+
expect(bus.locks.isLocked('/tmp/a.txt')).toBe(true)
|
|
152
|
+
|
|
153
|
+
bus.cleanupAgent(runId(1))
|
|
154
|
+
|
|
155
|
+
expect(bus.locks.isLocked('/tmp/a.txt')).toBe(false)
|
|
156
|
+
expect(bus.ownership.getOwner('/tmp/a.txt')).toBeUndefined()
|
|
157
|
+
expect(bus.breaker.getSnapshot(runId(1))?.state).toBe('closed')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('does not affect other runIds', async () => {
|
|
161
|
+
await bus.locks.acquire('/tmp/a.txt', runId(1))
|
|
162
|
+
await bus.locks.acquire('/tmp/b.txt', runId(2))
|
|
163
|
+
|
|
164
|
+
bus.cleanupAgent(runId(1))
|
|
165
|
+
|
|
166
|
+
expect(bus.locks.isLocked('/tmp/a.txt')).toBe(false)
|
|
167
|
+
expect(bus.locks.isLocked('/tmp/b.txt')).toBe(true)
|
|
168
|
+
expect(bus.locks.getHolder('/tmp/b.txt')).toBe(runId(2))
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('maintenance', () => {
|
|
173
|
+
it('sweeps expired locks', async () => {
|
|
174
|
+
vi.useFakeTimers()
|
|
175
|
+
const b = new AgentBus(makeLogger(), { lockTimeoutMs: 500, lockAcquireTimeoutMs: 30 })
|
|
176
|
+
await b.locks.acquire('/tmp/a.txt', runId(1))
|
|
177
|
+
vi.advanceTimersByTime(501)
|
|
178
|
+
|
|
179
|
+
b.maintenance()
|
|
180
|
+
expect(b.locks.isLocked('/tmp/a.txt')).toBe(false)
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
})
|
package/src/bus/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
DEFAULT_LOCK_TIMEOUT_MS,
|
|
6
6
|
DEFAULT_MAX_LOCKS_PER_AGENT,
|
|
7
7
|
} from '../constants/bus/index.js'
|
|
8
|
+
import { buildProbeContext } from '../probe/context.js'
|
|
9
|
+
import { type ProbeRegistry, probe as defaultProbeRegistry } from '../probe/registry.js'
|
|
8
10
|
import type { AgentBusEvent, AgentBusEventListener } from '../types/bus/index.js'
|
|
9
11
|
import type { RunId } from '../types/ids/index.js'
|
|
10
12
|
import type { Logger } from '../utils/logger.js'
|
|
@@ -39,10 +41,17 @@ export class AgentBus {
|
|
|
39
41
|
private readonly listeners: Set<AgentBusEventListener> = new Set()
|
|
40
42
|
private readonly log: Logger
|
|
41
43
|
private readonly config: AgentBusConfig
|
|
44
|
+
private readonly probes: ProbeRegistry
|
|
42
45
|
|
|
43
|
-
constructor(
|
|
46
|
+
constructor(
|
|
47
|
+
log: Logger,
|
|
48
|
+
config: Partial<AgentBusConfig> = {},
|
|
49
|
+
probeRegistry: ProbeRegistry = defaultProbeRegistry,
|
|
50
|
+
) {
|
|
44
51
|
this.config = { ...DEFAULT_AGENT_BUS_CONFIG, ...config }
|
|
45
52
|
this.log = log.child({ component: 'AgentBus' })
|
|
53
|
+
this.probes = probeRegistry
|
|
54
|
+
this.probes.setLogger(log)
|
|
46
55
|
|
|
47
56
|
const emitFn = (event: AgentBusEvent): void => this.emit(event)
|
|
48
57
|
|
|
@@ -70,16 +79,18 @@ export class AgentBus {
|
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
private emit(event: AgentBusEvent): void {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
this.probes.dispatch(event, buildProbeContext(), () => {
|
|
83
|
+
for (const listener of this.listeners) {
|
|
84
|
+
try {
|
|
85
|
+
listener(event)
|
|
86
|
+
} catch (error) {
|
|
87
|
+
this.log.error('event listener threw', {
|
|
88
|
+
eventType: event.type,
|
|
89
|
+
error: error instanceof Error ? error.message : String(error),
|
|
90
|
+
})
|
|
91
|
+
}
|
|
81
92
|
}
|
|
82
|
-
}
|
|
93
|
+
})
|
|
83
94
|
}
|
|
84
95
|
|
|
85
96
|
cleanupAgent(runId: RunId): void {
|