mcp-event-intelligence 0.8.2 → 0.9.0

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.
@@ -0,0 +1,238 @@
1
+ const REPORT_SCHEMA = 'event-intelligence.host-conformance.v1';
2
+
3
+ function assertFunction(value, name) {
4
+ if (typeof value !== 'function') {
5
+ throw new Error(`Host conformance adapter requires ${name}()`);
6
+ }
7
+ }
8
+
9
+ function validateHarness(harness) {
10
+ if (!harness || typeof harness !== 'object') {
11
+ throw new Error('createHarness() must return a harness object');
12
+ }
13
+ for (const method of [
14
+ 'createTrigger',
15
+ 'emitEvent',
16
+ 'deliveries',
17
+ 'restart',
18
+ 'inspectTrigger',
19
+ 'close',
20
+ ]) {
21
+ assertFunction(harness[method], method);
22
+ }
23
+ return harness;
24
+ }
25
+
26
+ function deliveryCount(rows, triggerId) {
27
+ return rows.filter((row) => row?.triggerId === triggerId).length;
28
+ }
29
+
30
+ async function runCase(id, execute) {
31
+ const startedAt = Date.now();
32
+ try {
33
+ const detail = await execute();
34
+ return Object.freeze({
35
+ id,
36
+ status: 'pass',
37
+ durationMs: Date.now() - startedAt,
38
+ ...(detail ? { detail } : {}),
39
+ });
40
+ } catch (error) {
41
+ return Object.freeze({
42
+ id,
43
+ status: 'fail',
44
+ durationMs: Date.now() - startedAt,
45
+ error: error instanceof Error ? error.message : String(error),
46
+ });
47
+ }
48
+ }
49
+
50
+ function requireCondition(condition, message) {
51
+ if (!condition) throw new Error(message);
52
+ }
53
+
54
+ /**
55
+ * Run the runtime-neutral EI host contract against an isolated host harness.
56
+ *
57
+ * The adapter owns all runtime-specific translation. The kit only uses a
58
+ * canonical value-change source and inspects externally visible deliveries,
59
+ * persisted trigger state and structured observability events.
60
+ */
61
+ export async function runHostConformance(
62
+ adapter,
63
+ { throwOnFailure = false } = {},
64
+ ) {
65
+ if (!adapter || typeof adapter !== 'object') {
66
+ throw new Error('Host conformance adapter is required');
67
+ }
68
+ assertFunction(adapter.createHarness, 'createHarness');
69
+
70
+ const observed = [];
71
+ const harness = validateHarness(await adapter.createHarness({
72
+ observability(event) {
73
+ observed.push(event);
74
+ },
75
+ }));
76
+
77
+ const results = [];
78
+ try {
79
+ results.push(await runCase('core.once-threshold', async () => {
80
+ await harness.createTrigger({
81
+ triggerId: 'conformance-once',
82
+ threshold: 1000,
83
+ oneShot: true,
84
+ });
85
+
86
+ await harness.emitEvent({
87
+ eventId: 'once-below',
88
+ value: 999,
89
+ secretProbe: 'ei-conformance-secret-probe',
90
+ });
91
+ requireCondition(
92
+ deliveryCount(await harness.deliveries(), 'conformance-once') === 0,
93
+ 'below-threshold event produced a delivery',
94
+ );
95
+
96
+ await harness.emitEvent({
97
+ eventId: 'once-match',
98
+ value: 1001,
99
+ secretProbe: 'ei-conformance-secret-probe',
100
+ });
101
+ requireCondition(
102
+ deliveryCount(await harness.deliveries(), 'conformance-once') === 1,
103
+ 'matching event did not produce exactly one delivery',
104
+ );
105
+
106
+ }));
107
+
108
+ results.push(await runCase('core.replay-dedup', async () => {
109
+ await harness.createTrigger({
110
+ triggerId: 'conformance-replay',
111
+ threshold: 1000,
112
+ oneShot: false,
113
+ maxFirings: 2,
114
+ });
115
+
116
+ await harness.emitEvent({
117
+ eventId: 'replay-same-id',
118
+ value: 1100,
119
+ secretProbe: 'ei-conformance-secret-probe',
120
+ });
121
+ await harness.emitEvent({
122
+ eventId: 'replay-same-id',
123
+ value: 1100,
124
+ secretProbe: 'ei-conformance-secret-probe',
125
+ });
126
+
127
+ requireCondition(
128
+ deliveryCount(await harness.deliveries(), 'conformance-replay') === 1,
129
+ 'replayed event produced a duplicate delivery on a persistent trigger',
130
+ );
131
+ const state = await harness.inspectTrigger('conformance-replay');
132
+ requireCondition(
133
+ Number(state?.fireCount) === 1,
134
+ `replayed event advanced fireCount to ${state?.fireCount}`,
135
+ );
136
+ }));
137
+
138
+ results.push(await runCase('core.restart-durability', async () => {
139
+ await harness.createTrigger({
140
+ triggerId: 'conformance-restart',
141
+ threshold: 1000,
142
+ oneShot: false,
143
+ maxFirings: 2,
144
+ });
145
+ await harness.restart();
146
+
147
+ await harness.emitEvent({
148
+ eventId: 'restart-1',
149
+ value: 1200,
150
+ secretProbe: 'ei-conformance-secret-probe',
151
+ });
152
+ requireCondition(
153
+ deliveryCount(await harness.deliveries(), 'conformance-restart') === 1,
154
+ 'trigger did not survive runtime restart',
155
+ );
156
+ }));
157
+
158
+ results.push(await runCase('core.max-firings', async () => {
159
+ await harness.emitEvent({
160
+ eventId: 'restart-2',
161
+ value: 1300,
162
+ secretProbe: 'ei-conformance-secret-probe',
163
+ });
164
+ await harness.emitEvent({
165
+ eventId: 'restart-3',
166
+ value: 1400,
167
+ secretProbe: 'ei-conformance-secret-probe',
168
+ });
169
+
170
+ requireCondition(
171
+ deliveryCount(await harness.deliveries(), 'conformance-restart') === 2,
172
+ 'maxFirings=2 did not cap external deliveries at two',
173
+ );
174
+
175
+ const state = await harness.inspectTrigger('conformance-restart');
176
+ requireCondition(Boolean(state), 'trigger state unavailable after firing');
177
+ requireCondition(
178
+ Number(state.fireCount) === 2,
179
+ `expected fireCount=2, got ${state.fireCount}`,
180
+ );
181
+ }));
182
+
183
+ results.push(await runCase('observability.required-events', async () => {
184
+ const names = new Set(observed.map((event) => event?.event));
185
+ for (const name of [
186
+ 'ei.trigger.created',
187
+ 'ei.match.matched',
188
+ 'ei.wake.delivered',
189
+ ]) {
190
+ requireCondition(
191
+ names.has(name),
192
+ `missing required observability event ${name}`,
193
+ );
194
+ }
195
+
196
+ const serialized = JSON.stringify(observed);
197
+ requireCondition(
198
+ !serialized.includes('ei-conformance-secret-probe'),
199
+ 'observability leaked event payload content',
200
+ );
201
+ }));
202
+ } finally {
203
+ await harness.close();
204
+ }
205
+
206
+ const failed = results.filter((result) => result.status === 'fail');
207
+ const report = Object.freeze({
208
+ schema: REPORT_SCHEMA,
209
+ adapter: String(adapter.name ?? 'unnamed-host'),
210
+ passed: failed.length === 0,
211
+ summary: Object.freeze({
212
+ total: results.length,
213
+ passed: results.length - failed.length,
214
+ failed: failed.length,
215
+ }),
216
+ results: Object.freeze(results),
217
+ observability: Object.freeze({
218
+ captured: observed.length,
219
+ eventNames: Object.freeze([...new Set(
220
+ observed.map((event) => event?.event).filter(Boolean),
221
+ )].sort()),
222
+ }),
223
+ });
224
+
225
+ if (throwOnFailure && !report.passed) {
226
+ const error = new Error(
227
+ `Event Intelligence host conformance failed: ${failed
228
+ .map((item) => item.id)
229
+ .join(', ')}`,
230
+ );
231
+ error.report = report;
232
+ throw error;
233
+ }
234
+
235
+ return report;
236
+ }
237
+
238
+ export const HOST_CONFORMANCE_REPORT_SCHEMA = REPORT_SCHEMA;
@@ -2,6 +2,9 @@ import type {
2
2
  ActivationEnvelope,
3
3
  TriggerPlanInput,
4
4
  } from '../dist/src/intelligenceProtocol/index.js';
5
+ import type {
6
+ EventIntelligenceObservabilitySink,
7
+ } from './lib/observability.d.mts';
5
8
 
6
9
  export interface HostMcpClientLike {
7
10
  request(
@@ -130,6 +133,8 @@ export interface EventIntelligenceHostOptions {
130
133
  /** Optional runtime-specific dispatchers. */
131
134
  wakeHandlers?: Map<string, HostWakeHandler> | Record<string, HostWakeHandler>;
132
135
  semanticEvaluator?: unknown;
136
+ /** Runtime-neutral structured operational events. Sink failures never affect EI semantics. */
137
+ observability?: EventIntelligenceObservabilitySink;
133
138
  }
134
139
 
135
140
  export interface EventIntelligenceScopedHost {
@@ -127,6 +127,7 @@ export async function createEventIntelligenceHost({
127
127
  wake,
128
128
  wakeHandlers = {},
129
129
  semanticEvaluator,
130
+ observability,
130
131
  } = {}) {
131
132
  const explicitConnections = mcpClients.map(normalizeMcpConnection);
132
133
  const runtime = await createLocalEventIntelligenceRuntime({
@@ -140,7 +141,9 @@ export async function createEventIntelligenceHost({
140
141
  wakeHandlers,
141
142
  semanticEvaluator,
142
143
  store,
144
+ observability,
143
145
  });
146
+ const observer = runtime.observability;
144
147
 
145
148
  const registryManaged = new Set();
146
149
  let unsubscribe = null;
@@ -212,6 +215,21 @@ export async function createEventIntelligenceHost({
212
215
  }
213
216
  }
214
217
 
218
+ await Promise.all(outcomes.map((outcome) => observer.emit({
219
+ event: 'ei.source.registry_update',
220
+ level:
221
+ outcome.status === 'error'
222
+ ? 'error'
223
+ : outcome.status === 'ignored_no_events' ||
224
+ outcome.status === 'already_attached'
225
+ ? 'debug'
226
+ : 'info',
227
+ connectionId: outcome.connectionId,
228
+ status: outcome.status,
229
+ ...(outcome.error ? { error: new Error(outcome.error) } : {}),
230
+ ...(outcome.events ? { metadata: { eventNames: outcome.events } } : {}),
231
+ })));
232
+
215
233
  return outcomes;
216
234
  };
217
235
 
@@ -241,10 +259,11 @@ export async function createEventIntelligenceHost({
241
259
  if (typeof mcpRegistry.subscribe === 'function') {
242
260
  const maybeUnsubscribe = mcpRegistry.subscribe(() => {
243
261
  refreshMcpRegistry().catch((error) => {
244
- console.error(JSON.stringify({
245
- message: 'event_intelligence_mcp_registry_refresh_failed',
246
- error: error instanceof Error ? error.message : String(error),
247
- }));
262
+ void observer.emit({
263
+ event: 'ei.source.refresh_failed',
264
+ level: 'error',
265
+ error,
266
+ });
248
267
  });
249
268
  });
250
269
  if (typeof maybeUnsubscribe === 'function') {
@@ -10,6 +10,7 @@ export class CompositeEventConsumer {
10
10
  wakeCoordinators = null,
11
11
  derivedEventCoordinator = null,
12
12
  maxDerivedDepth = 16,
13
+ observability = null,
13
14
  }) {
14
15
  this.store = store;
15
16
  this.triggerEngine = triggerEngine;
@@ -19,6 +20,7 @@ export class CompositeEventConsumer {
19
20
  : new Map();
20
21
  this.derivedEventCoordinator = derivedEventCoordinator;
21
22
  this.maxDerivedDepth = Math.max(1, Number(maxDerivedDepth) || 16);
23
+ this.observability = observability;
22
24
  }
23
25
 
24
26
  coordinatorFor(runtime) {
@@ -58,6 +60,15 @@ export class CompositeEventConsumer {
58
60
  for (const result of results) {
59
61
  if (!result.matched || !result.match) continue;
60
62
 
63
+ await this.observability?.emit({
64
+ event: 'ei.match.matched',
65
+ level: 'info',
66
+ traceId: event.traceId,
67
+ triggerId: result.match.triggerId,
68
+ matchId: result.match.matchId,
69
+ status: result.match.status,
70
+ });
71
+
61
72
  const definition = (await this.store.listTriggers()).find(
62
73
  (candidate) =>
63
74
  candidate.triggerId === result.match.triggerId &&
@@ -72,6 +83,14 @@ export class CompositeEventConsumer {
72
83
  status: 'trigger_definition_missing',
73
84
  runtimeReceiptId: null,
74
85
  });
86
+ await this.observability?.emit({
87
+ event: 'ei.match.definition_missing',
88
+ level: 'error',
89
+ traceId: event.traceId,
90
+ triggerId: result.triggerId,
91
+ matchId: result.match.matchId,
92
+ status: 'trigger_definition_missing',
93
+ });
75
94
  continue;
76
95
  }
77
96
 
@@ -131,6 +150,15 @@ export class CompositeEventConsumer {
131
150
  status: 'runtime_unconfigured',
132
151
  runtimeReceiptId: null,
133
152
  });
153
+ await this.observability?.emit({
154
+ event: 'ei.wake.runtime_unconfigured',
155
+ level: 'warn',
156
+ traceId: event.traceId,
157
+ triggerId: result.triggerId,
158
+ matchId: result.match.matchId,
159
+ status: 'runtime_unconfigured',
160
+ metadata: { runtime: definition.target.runtime },
161
+ });
134
162
  continue;
135
163
  }
136
164
 
@@ -31,6 +31,7 @@ export class CompositeWakeCoordinator {
31
31
  maxAttempts = 5,
32
32
  retryBaseDelayMs = 1000,
33
33
  retryMaxDelayMs = 60000,
34
+ observability = null,
34
35
  }) {
35
36
  this.store = store;
36
37
  this.triggerEngine = triggerEngine;
@@ -45,6 +46,7 @@ export class CompositeWakeCoordinator {
45
46
  this.retryBaseDelayMs,
46
47
  Number(retryMaxDelayMs) || 60000,
47
48
  );
49
+ this.observability = observability;
48
50
 
49
51
  for (const method of [
50
52
  'ensureWakeDelivery',
@@ -101,6 +103,16 @@ export class CompositeWakeCoordinator {
101
103
  composite: true,
102
104
  },
103
105
  });
106
+ await this.observability?.emit({
107
+ event: 'ei.wake.delivered',
108
+ level: 'info',
109
+ traceId: delivered.traceId,
110
+ triggerId: match.triggerId,
111
+ matchId: match.matchId,
112
+ wakeId,
113
+ status: 'wake_delivered',
114
+ metadata: { recoveredFromDeliveryState: true },
115
+ });
104
116
  }
105
117
 
106
118
  if (match.status !== 'fired') {
@@ -193,6 +205,15 @@ export class CompositeWakeCoordinator {
193
205
  composite: true,
194
206
  },
195
207
  });
208
+ await this.observability?.emit({
209
+ event: 'ei.wake.queued',
210
+ level: 'debug',
211
+ traceId: queued.traceId,
212
+ triggerId: match.triggerId,
213
+ matchId: match.matchId,
214
+ wakeId,
215
+ status: 'wake_queued',
216
+ });
196
217
  }
197
218
  return {
198
219
  status: 'wake_queued',
@@ -272,6 +293,15 @@ export class CompositeWakeCoordinator {
272
293
  composite: true,
273
294
  },
274
295
  });
296
+ await this.observability?.emit({
297
+ event: 'ei.wake.queued',
298
+ level: 'debug',
299
+ traceId: queued.traceId,
300
+ triggerId: match.triggerId,
301
+ matchId: match.matchId,
302
+ wakeId,
303
+ status: 'wake_queued',
304
+ });
275
305
  }
276
306
 
277
307
  try {
@@ -322,6 +352,17 @@ export class CompositeWakeCoordinator {
322
352
  composite: true,
323
353
  },
324
354
  });
355
+ await this.observability?.emit({
356
+ event: 'ei.wake.delivered',
357
+ level: 'info',
358
+ traceId: delivered.traceId,
359
+ triggerId: match.triggerId,
360
+ matchId: match.matchId,
361
+ wakeId,
362
+ status: 'wake_delivered',
363
+ attempt: delivery?.attemptCount ?? claim.attemptCount,
364
+ metadata: { duplicate: receipt.duplicate === true },
365
+ });
325
366
 
326
367
  await this.triggerEngine.markFired(match.matchId, wakeId);
327
368
 
@@ -365,6 +406,18 @@ export class CompositeWakeCoordinator {
365
406
  composite: true,
366
407
  },
367
408
  });
409
+ await this.observability?.emit({
410
+ event: 'ei.wake.retry_scheduled',
411
+ level: 'warn',
412
+ traceId: queued.traceId,
413
+ triggerId: match.triggerId,
414
+ matchId: match.matchId,
415
+ wakeId,
416
+ status: 'retry_scheduled',
417
+ attempt: delivery?.attemptCount ?? claim.attemptCount,
418
+ nextAttemptAt: delivery?.nextAttemptAt ?? undefined,
419
+ error,
420
+ });
368
421
  return {
369
422
  status: 'retry_scheduled',
370
423
  wake: persistedQueued,
@@ -398,6 +451,17 @@ export class CompositeWakeCoordinator {
398
451
  composite: true,
399
452
  },
400
453
  });
454
+ await this.observability?.emit({
455
+ event: 'ei.wake.dead_letter',
456
+ level: 'error',
457
+ traceId: deadLetter.traceId,
458
+ triggerId: match.triggerId,
459
+ matchId: match.matchId,
460
+ wakeId,
461
+ status: 'dead_letter',
462
+ attempt: delivery?.attemptCount ?? claim.attemptCount,
463
+ error,
464
+ });
401
465
  return {
402
466
  status: 'dead_letter',
403
467
  wake: deadLetter,
@@ -48,6 +48,9 @@ import {
48
48
  import {
49
49
  ActivationHydrator,
50
50
  } from './activation-hydrator.mjs';
51
+ import {
52
+ createObservabilityEmitter,
53
+ } from './observability.mjs';
51
54
 
52
55
  const REQUIRED_STORE_METHODS = [
53
56
  'putTrigger',
@@ -114,7 +117,9 @@ export async function createLocalEventIntelligenceRuntime({
114
117
  wake,
115
118
  semanticEvaluator,
116
119
  store: providedStore,
120
+ observability,
117
121
  } = {}) {
122
+ const observer = createObservabilityEmitter(observability);
118
123
  const dataDir = env.DATA_DIR ?? './data';
119
124
  const rootStore = providedStore ?? new PersistentEventStore(dataDir);
120
125
  const restored = await initializeStore(rootStore);
@@ -212,6 +217,7 @@ export async function createLocalEventIntelligenceRuntime({
212
217
  triggerEngine,
213
218
  deliverer: createSignedRuntimeWakeDeliverer(target),
214
219
  packetBuilder,
220
+ observability: observer,
215
221
  ...deliveryOptions,
216
222
  }),
217
223
  );
@@ -224,6 +230,7 @@ export async function createLocalEventIntelligenceRuntime({
224
230
  store,
225
231
  triggerEngine,
226
232
  packetBuilder,
233
+ observability: observer,
227
234
  ...deliveryOptions,
228
235
  deliverer: async (packet) => {
229
236
  const activation = await activationHydrator.hydrateWake(packet.wake_id);
@@ -253,6 +260,7 @@ export async function createLocalEventIntelligenceRuntime({
253
260
  store,
254
261
  triggerEngine,
255
262
  packetBuilder,
263
+ observability: observer,
256
264
  ...deliveryOptions,
257
265
  deliverer: async (packet) => {
258
266
  const activation = await activationHydrator.hydrateWake(packet.wake_id);
@@ -281,18 +289,21 @@ export async function createLocalEventIntelligenceRuntime({
281
289
  wakeCoordinators,
282
290
  derivedEventCoordinator,
283
291
  maxDerivedDepth: Number(env.MAX_DERIVED_EVENT_DEPTH ?? 16),
292
+ observability: observer,
284
293
  });
285
294
 
286
295
  const temporalScheduler = new TemporalDeadlineScheduler({
287
296
  store,
288
297
  compositeEventConsumer,
289
298
  intervalMs: Number(env.TEMPORAL_TICK_MS ?? 1000),
299
+ observability: observer,
290
300
  });
291
301
  temporalScheduler.start();
292
302
 
293
303
  const triggerControl = new TriggerControlPlane({
294
304
  store,
295
305
  triggerEngine,
306
+ observability: observer,
296
307
  });
297
308
  const triggerInspector = new TriggerInspector({ store });
298
309
 
@@ -302,6 +313,7 @@ export async function createLocalEventIntelligenceRuntime({
302
313
  store,
303
314
  resolveCoordinator: coordinatorFor,
304
315
  intervalMs: Number(env.WAKE_RETRY_TICK_MS ?? 1000),
316
+ observability: observer,
305
317
  });
306
318
  wakeRetryScheduler.start();
307
319
 
@@ -354,13 +366,38 @@ export async function createLocalEventIntelligenceRuntime({
354
366
  principal_id: 'event-intelligence:host-mcp-client',
355
367
  },
356
368
  ),
369
+ observability: observer,
357
370
  });
358
371
 
359
372
  const discovery = await mcpEventsClient.discoverAll();
360
373
  mcpEventsClient.start();
361
374
 
375
+ for (const source of discovery) {
376
+ await observer.emit({
377
+ event: source.status === 'error'
378
+ ? 'ei.source.discovery_failed'
379
+ : 'ei.source.discovered',
380
+ level: source.status === 'error' ? 'warn' : 'info',
381
+ connectionId: source.connectionId,
382
+ status: source.status,
383
+ ...(source.error ? { error: new Error(source.error) } : {}),
384
+ ...(source.events ? { metadata: { eventNames: source.events } } : {}),
385
+ });
386
+ }
387
+ await observer.emit({
388
+ event: 'ei.lifecycle.started',
389
+ level: 'info',
390
+ metadata: {
391
+ restoredScopes:
392
+ typeof rootStore.listScopeIds === 'function'
393
+ ? await rootStore.listScopeIds()
394
+ : [DEFAULT_EVENT_SCOPE_ID],
395
+ },
396
+ });
397
+
362
398
  return {
363
399
  restored,
400
+ observability: observer,
364
401
  storeCapabilities,
365
402
  store: defaultContext.store,
366
403
  evaluator,
@@ -378,6 +415,7 @@ export async function createLocalEventIntelligenceRuntime({
378
415
  return [...scopeContexts.keys()].sort();
379
416
  },
380
417
  async close() {
418
+ await observer.emit({ event: 'ei.lifecycle.closing', level: 'debug' });
381
419
  await mcpEventsClient.close();
382
420
  const contexts = [...scopeContexts.values()];
383
421
  await Promise.all(contexts.map((context) => context.close()));
@@ -390,6 +428,7 @@ export async function createLocalEventIntelligenceRuntime({
390
428
  await store.drain();
391
429
  }
392
430
  }
431
+ await observer.emit({ event: 'ei.lifecycle.closed', level: 'info' });
393
432
  },
394
433
  };
395
434
  }
@@ -286,6 +286,7 @@ export class McpEventsClientManager {
286
286
  resolveScope = null,
287
287
  connections = [],
288
288
  now = () => new Date(),
289
+ observability = null,
289
290
  }) {
290
291
  this.store = store;
291
292
  this.compositeEventConsumer = compositeEventConsumer;
@@ -293,6 +294,7 @@ export class McpEventsClientManager {
293
294
  this.resolveScope = resolveScope;
294
295
  this.connections = new Map();
295
296
  this.now = now;
297
+ this.observability = observability;
296
298
  this.descriptors = new Map();
297
299
  this.profiles = new Map();
298
300
  this.timers = new Map();
@@ -877,6 +879,15 @@ export class McpEventsClientManager {
877
879
  connection.connectionId,
878
880
  error instanceof Error ? error.message : String(error),
879
881
  );
882
+ void this.observability?.emit({
883
+ event: 'ei.source.delivery_failed',
884
+ level: 'warn',
885
+ scopeId: connection.scopeId,
886
+ connectionId: connection.connectionId,
887
+ serverId: connection.serverId,
888
+ eventName: subscription.eventName,
889
+ error,
890
+ });
880
891
  };
881
892
 
882
893
  const onTerminated = async (detail) => {
@@ -887,6 +898,15 @@ export class McpEventsClientManager {
887
898
  typeof detail === 'string' ? detail : JSON.stringify(detail),
888
899
  );
889
900
  }
901
+ void this.observability?.emit({
902
+ event: 'ei.source.delivery_terminated',
903
+ level: detail ? 'warn' : 'debug',
904
+ scopeId: connection.scopeId,
905
+ connectionId: connection.connectionId,
906
+ serverId: connection.serverId,
907
+ eventName: subscription.eventName,
908
+ status: 'terminated',
909
+ });
890
910
  };
891
911
 
892
912
  const open =
@@ -1054,6 +1074,14 @@ export class McpEventsClientManager {
1054
1074
  } catch (error) {
1055
1075
  const message = error instanceof Error ? error.message : String(error);
1056
1076
  this.lastErrors.set(connection.connectionId, message);
1077
+ await this.observability?.emit({
1078
+ event: 'ei.source.poll_failed',
1079
+ level: 'warn',
1080
+ scopeId: connection.scopeId,
1081
+ connectionId: connection.connectionId,
1082
+ serverId: connection.serverId,
1083
+ error,
1084
+ });
1057
1085
  results.push({
1058
1086
  connectionId: connection.connectionId,
1059
1087
  error: message,
@@ -1097,6 +1125,14 @@ export class McpEventsClientManager {
1097
1125
  connection.connectionId,
1098
1126
  error instanceof Error ? error.message : String(error),
1099
1127
  );
1128
+ await this.observability?.emit({
1129
+ event: 'ei.source.poll_failed',
1130
+ level: 'warn',
1131
+ scopeId: connection.scopeId,
1132
+ connectionId: connection.connectionId,
1133
+ serverId: connection.serverId,
1134
+ error,
1135
+ });
1100
1136
  } finally {
1101
1137
  if (
1102
1138
  this.started &&