mcp-event-intelligence 0.1.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/LICENSE +201 -0
  3. package/README.md +298 -0
  4. package/SECURITY.md +42 -0
  5. package/bin/mcp-event-intelligence.mjs +52 -0
  6. package/dist/src/composite/engine.d.ts +87 -0
  7. package/dist/src/composite/engine.js +667 -0
  8. package/dist/src/composite/temporal.d.ts +32 -0
  9. package/dist/src/composite/temporal.js +317 -0
  10. package/dist/src/intelligenceProtocol/audit.d.ts +19 -0
  11. package/dist/src/intelligenceProtocol/audit.js +56 -0
  12. package/dist/src/intelligenceProtocol/canonical.d.ts +2 -0
  13. package/dist/src/intelligenceProtocol/canonical.js +21 -0
  14. package/dist/src/intelligenceProtocol/index.d.ts +12 -0
  15. package/dist/src/intelligenceProtocol/index.js +8 -0
  16. package/dist/src/intelligenceProtocol/invariants.d.ts +3 -0
  17. package/dist/src/intelligenceProtocol/invariants.js +36 -0
  18. package/dist/src/intelligenceProtocol/lifecycle.d.ts +4 -0
  19. package/dist/src/intelligenceProtocol/lifecycle.js +24 -0
  20. package/dist/src/intelligenceProtocol/mapper.d.ts +14 -0
  21. package/dist/src/intelligenceProtocol/mapper.js +28 -0
  22. package/dist/src/intelligenceProtocol/schemas.d.ts +325 -0
  23. package/dist/src/intelligenceProtocol/schemas.js +182 -0
  24. package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +488 -0
  25. package/dist/src/intelligenceProtocol/triggerSchemas.js +379 -0
  26. package/dist/src/intelligenceProtocol/version.d.ts +7 -0
  27. package/dist/src/intelligenceProtocol/version.js +7 -0
  28. package/dist/src/mcpEvents/consumer.d.ts +7 -0
  29. package/dist/src/mcpEvents/consumer.js +14 -0
  30. package/dist/src/mcpEvents/server.d.ts +41 -0
  31. package/dist/src/mcpEvents/server.js +122 -0
  32. package/dist/src/protocol/types.d.ts +37 -0
  33. package/dist/src/protocol/types.js +1 -0
  34. package/dist/src/semantic/conditionEngine.d.ts +7 -0
  35. package/dist/src/semantic/conditionEngine.js +65 -0
  36. package/dist/src/semantic/types.d.ts +31 -0
  37. package/dist/src/semantic/types.js +1 -0
  38. package/dist/src/semantic/typesafeJevEvaluator.d.ts +23 -0
  39. package/dist/src/semantic/typesafeJevEvaluator.js +95 -0
  40. package/package.json +85 -0
  41. package/scripts/host-integration.d.mts +86 -0
  42. package/scripts/host-integration.mjs +216 -0
  43. package/scripts/lib/composite-event-consumer.mjs +172 -0
  44. package/scripts/lib/composite-wake-coordinator.mjs +185 -0
  45. package/scripts/lib/derived-contract-registry.mjs +210 -0
  46. package/scripts/lib/derived-event-coordinator.mjs +242 -0
  47. package/scripts/lib/event-processor.mjs +409 -0
  48. package/scripts/lib/generic-runtime-wake.mjs +76 -0
  49. package/scripts/lib/github-mcp-events-adapter.mjs +66 -0
  50. package/scripts/lib/github-webhook.mjs +88 -0
  51. package/scripts/lib/http-wake-deliverer.mjs +31 -0
  52. package/scripts/lib/local-event-intelligence-runtime.mjs +193 -0
  53. package/scripts/lib/mcp-events-client.mjs +519 -0
  54. package/scripts/lib/persistent-event-store.mjs +769 -0
  55. package/scripts/lib/temporal-deadline-scheduler.mjs +111 -0
  56. package/scripts/lib/trigger-control-plane.mjs +939 -0
  57. package/scripts/lib/trigger-inspector.mjs +631 -0
  58. package/scripts/lib/wake-signature.mjs +10 -0
  59. package/scripts/mcp-stdio-server.mjs +380 -0
  60. package/scripts/mcp-stdio.mjs +3 -0
  61. package/scripts/run-tests.mjs +51 -0
  62. package/scripts/service.mjs +756 -0
  63. package/server.json +68 -0
@@ -0,0 +1,769 @@
1
+ import { appendFile, mkdir, readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {
4
+ AuditChain,
5
+ parseAuditRecord,
6
+ parseCompositeTriggerDefinition,
7
+ parseTriggerMatchRecord,
8
+ parseDerivedEventRecord,
9
+ McpEventOccurrenceSchema,
10
+ } from '../../dist/src/intelligenceProtocol/index.js';
11
+
12
+ async function readJsonLines(filePath) {
13
+ try {
14
+ const raw = await readFile(filePath, 'utf8');
15
+ return raw
16
+ .split('\n')
17
+ .filter(Boolean)
18
+ .map((line) => JSON.parse(line));
19
+ } catch (error) {
20
+ if (error?.code === 'ENOENT') return [];
21
+ throw error;
22
+ }
23
+ }
24
+
25
+ export class PersistentEventStore {
26
+ #queue = Promise.resolve();
27
+ #eventKeys = new Set();
28
+ #events = [];
29
+ #decisions = [];
30
+ #wakes = [];
31
+ #triggers = new Map();
32
+ #triggerStates = new Map();
33
+ #eventSources = new Map();
34
+ #triggerMatchHistory = [];
35
+ #triggerMatches = new Map();
36
+ #mcpOccurrences = [];
37
+ #mcpOccurrenceKeys = new Set();
38
+ #mcpClientStates = new Map();
39
+ #temporalDeadlines = new Map();
40
+ #derivedEvents = new Map();
41
+ #derivedContracts = new Map();
42
+ #auditChain = new AuditChain();
43
+
44
+ constructor(dataDir) {
45
+ this.dataDir = dataDir;
46
+ this.files = {
47
+ events: path.join(dataDir, 'events.jsonl'),
48
+ decisions: path.join(dataDir, 'decisions.jsonl'),
49
+ wakes: path.join(dataDir, 'wakes.jsonl'),
50
+ triggers: path.join(dataDir, 'triggers.jsonl'),
51
+ triggerStates: path.join(dataDir, 'trigger-states.jsonl'),
52
+ eventSources: path.join(dataDir, 'event-sources.jsonl'),
53
+ triggerMatches: path.join(dataDir, 'trigger-matches.jsonl'),
54
+ mcpOccurrences: path.join(dataDir, 'mcp-events.jsonl'),
55
+ mcpClientStates: path.join(dataDir, 'mcp-client-states.jsonl'),
56
+ temporalDeadlines: path.join(dataDir, 'temporal-deadlines.jsonl'),
57
+ derivedEvents: path.join(dataDir, 'derived-events.jsonl'),
58
+ derivedContracts: path.join(dataDir, 'derived-contracts.jsonl'),
59
+ audit: path.join(dataDir, 'audit.jsonl'),
60
+ };
61
+ }
62
+
63
+ async init() {
64
+ await mkdir(this.dataDir, { recursive: true });
65
+
66
+ const [
67
+ events,
68
+ decisions,
69
+ wakes,
70
+ triggers,
71
+ triggerStates,
72
+ eventSources,
73
+ triggerMatches,
74
+ mcpOccurrences,
75
+ mcpClientStates,
76
+ temporalDeadlines,
77
+ derivedEvents,
78
+ derivedContracts,
79
+ audit,
80
+ ] = await Promise.all([
81
+ readJsonLines(this.files.events),
82
+ readJsonLines(this.files.decisions),
83
+ readJsonLines(this.files.wakes),
84
+ readJsonLines(this.files.triggers),
85
+ readJsonLines(this.files.triggerStates),
86
+ readJsonLines(this.files.eventSources),
87
+ readJsonLines(this.files.triggerMatches),
88
+ readJsonLines(this.files.mcpOccurrences),
89
+ readJsonLines(this.files.mcpClientStates),
90
+ readJsonLines(this.files.temporalDeadlines),
91
+ readJsonLines(this.files.derivedEvents),
92
+ readJsonLines(this.files.derivedContracts),
93
+ readJsonLines(this.files.audit),
94
+ ]);
95
+
96
+ this.#events = events;
97
+ this.#decisions = decisions;
98
+ this.#wakes = wakes;
99
+
100
+ this.#triggers = new Map();
101
+ for (const raw of triggers) {
102
+ const definition = parseCompositeTriggerDefinition(raw);
103
+ this.#triggers.set(this.#triggerKey(definition), definition);
104
+ }
105
+
106
+ this.#triggerStates = new Map();
107
+ for (const raw of triggerStates) {
108
+ if (!raw?.triggerId || !raw?.version) continue;
109
+ this.#triggerStates.set(
110
+ `${raw.triggerId}@${raw.version}`,
111
+ {
112
+ triggerId: String(raw.triggerId),
113
+ version: String(raw.version),
114
+ status: ['active', 'paused', 'completed', 'expired', 'deleted'].includes(raw.status)
115
+ ? raw.status
116
+ : 'active',
117
+ updatedAt: String(raw.updatedAt || new Date(0).toISOString()),
118
+ actor: raw.actor ?? null,
119
+ owner: raw.owner ?? null,
120
+ connectionIds: Array.isArray(raw.connectionIds)
121
+ ? raw.connectionIds.map(String)
122
+ : [],
123
+ fireCount: Number.isInteger(raw.fireCount) && raw.fireCount >= 0
124
+ ? raw.fireCount
125
+ : 0,
126
+ lastFiredAt: raw.lastFiredAt ? String(raw.lastFiredAt) : null,
127
+ revision: Number.isInteger(raw.revision) && raw.revision >= 0
128
+ ? raw.revision
129
+ : 0,
130
+ },
131
+ );
132
+ }
133
+
134
+ this.#eventSources = new Map();
135
+ for (const raw of eventSources) {
136
+ if (!raw?.sourceId || !raw?.eventName || !raw?.connectionId) continue;
137
+ this.#eventSources.set(String(raw.sourceId), {
138
+ sourceId: String(raw.sourceId),
139
+ connectionId: String(raw.connectionId),
140
+ serverId: String(raw.serverId || raw.connectionId),
141
+ eventName: String(raw.eventName),
142
+ description: raw.description ? String(raw.description) : undefined,
143
+ delivery: Array.isArray(raw.delivery) ? raw.delivery.map(String) : ['webhook'],
144
+ inputSchema: raw.inputSchema && typeof raw.inputSchema === 'object' ? raw.inputSchema : undefined,
145
+ payloadSchema: raw.payloadSchema && typeof raw.payloadSchema === 'object' ? raw.payloadSchema : undefined,
146
+ enabled: raw.enabled !== false,
147
+ experimental: raw.experimental !== false,
148
+ metadata: raw.metadata && typeof raw.metadata === 'object' ? raw.metadata : undefined,
149
+ updatedAt: String(raw.updatedAt || new Date(0).toISOString()),
150
+ });
151
+ }
152
+
153
+ this.#triggerMatchHistory = [];
154
+ this.#triggerMatches = new Map();
155
+ for (const raw of triggerMatches) {
156
+ const record = parseTriggerMatchRecord(raw);
157
+ this.#triggerMatchHistory.push(record);
158
+ this.#triggerMatches.set(record.matchId, record);
159
+ }
160
+
161
+ this.#mcpOccurrences = [];
162
+ this.#mcpOccurrenceKeys = new Set();
163
+ for (const raw of mcpOccurrences) {
164
+ const record = {
165
+ sequence: Number(raw.sequence),
166
+ serverId: String(raw.serverId),
167
+ event: McpEventOccurrenceSchema.parse(raw.event),
168
+ };
169
+ if (!Number.isInteger(record.sequence) || record.sequence < 1) {
170
+ throw new Error('Invalid persisted MCP event sequence');
171
+ }
172
+ this.#mcpOccurrences.push(record);
173
+ this.#mcpOccurrenceKeys.add(
174
+ `${record.serverId}:${record.event.eventId}`,
175
+ );
176
+ }
177
+ this.#mcpOccurrences.sort((a, b) => a.sequence - b.sequence);
178
+
179
+ this.#mcpClientStates = new Map();
180
+ for (const raw of mcpClientStates) {
181
+ if (!raw?.connectionId || !raw?.eventName) continue;
182
+ const state = {
183
+ connectionId: String(raw.connectionId),
184
+ serverId: String(raw.serverId || raw.connectionId),
185
+ eventName: String(raw.eventName),
186
+ cursor: raw.cursor === null || raw.cursor === undefined
187
+ ? null
188
+ : String(raw.cursor),
189
+ updatedAt: String(raw.updatedAt || new Date(0).toISOString()),
190
+ ...(raw.lastEventAt ? { lastEventAt: String(raw.lastEventAt) } : {}),
191
+ ...(raw.lastError ? { lastError: String(raw.lastError) } : {}),
192
+ };
193
+ this.#mcpClientStates.set(
194
+ `${state.connectionId}::${state.eventName}`,
195
+ state,
196
+ );
197
+ }
198
+
199
+ this.#temporalDeadlines = new Map();
200
+ for (const raw of temporalDeadlines) {
201
+ if (!raw?.deadlineId || !raw?.triggerId || !raw?.matchId || !raw?.conditionId) {
202
+ continue;
203
+ }
204
+ const deadline = {
205
+ deadlineId: String(raw.deadlineId),
206
+ triggerId: String(raw.triggerId),
207
+ triggerVersion: String(raw.triggerVersion || '1'),
208
+ matchId: String(raw.matchId),
209
+ conditionId: String(raw.conditionId),
210
+ dueAt: String(raw.dueAt),
211
+ status: ['pending', 'cancelled', 'fired'].includes(raw.status)
212
+ ? raw.status
213
+ : 'pending',
214
+ createdAt: String(raw.createdAt || new Date(0).toISOString()),
215
+ updatedAt: String(raw.updatedAt || new Date(0).toISOString()),
216
+ };
217
+ this.#temporalDeadlines.set(deadline.deadlineId, deadline);
218
+ }
219
+
220
+ this.#derivedEvents = new Map();
221
+ for (const raw of derivedEvents) {
222
+ const record = parseDerivedEventRecord(raw);
223
+ this.#derivedEvents.set(
224
+ `${record.event.serverId ?? ''}::${record.event.sourceEventId}`,
225
+ record,
226
+ );
227
+ }
228
+
229
+ this.#derivedContracts = new Map();
230
+ for (const raw of derivedContracts) {
231
+ if (!raw?.eventName || !raw?.contractVersion || !raw?.schemaFingerprint) continue;
232
+ const record = {
233
+ eventName: String(raw.eventName),
234
+ contractVersion: String(raw.contractVersion),
235
+ payloadSchema: raw.payloadSchema && typeof raw.payloadSchema === 'object'
236
+ ? raw.payloadSchema
237
+ : {},
238
+ schemaFingerprint: String(raw.schemaFingerprint),
239
+ producers: Array.isArray(raw.producers)
240
+ ? raw.producers.map((producer) => ({
241
+ triggerId: String(producer.triggerId),
242
+ triggerVersion: String(producer.triggerVersion),
243
+ }))
244
+ : [],
245
+ createdAt: String(raw.createdAt || new Date(0).toISOString()),
246
+ updatedAt: String(raw.updatedAt || new Date(0).toISOString()),
247
+ };
248
+ this.#derivedContracts.set(
249
+ `${record.eventName}@${record.contractVersion}`,
250
+ record,
251
+ );
252
+ }
253
+
254
+ this.#auditChain = new AuditChain(audit.map(parseAuditRecord));
255
+
256
+ for (const record of events) {
257
+ this.#eventKeys.add(this.#eventKey(record.lineage));
258
+ }
259
+
260
+ if (!(await this.#auditChain.verify())) {
261
+ throw new Error('Persistent audit chain verification failed');
262
+ }
263
+
264
+ return {
265
+ events: events.length,
266
+ decisions: decisions.length,
267
+ wakes: wakes.length,
268
+ triggers: this.#triggers.size,
269
+ triggerStates: this.#triggerStates.size,
270
+ eventSources: this.#eventSources.size,
271
+ triggerMatches: this.#triggerMatches.size,
272
+ triggerMatchRevisions: this.#triggerMatchHistory.length,
273
+ mcpOccurrences: this.#mcpOccurrences.length,
274
+ mcpClientStates: this.#mcpClientStates.size,
275
+ temporalDeadlines: this.#temporalDeadlines.size,
276
+ derivedEvents: this.#derivedEvents.size,
277
+ derivedContracts: this.#derivedContracts.size,
278
+ audit: audit.length,
279
+ };
280
+ }
281
+
282
+ async registerEvent(record) {
283
+ return this.#serialized(async () => {
284
+ const key = this.#eventKey(record.lineage);
285
+ if (this.#eventKeys.has(key)) return false;
286
+
287
+ this.#eventKeys.add(key);
288
+ this.#events.push(record);
289
+ await appendFile(this.files.events, `${JSON.stringify(record)}\n`, 'utf8');
290
+ return true;
291
+ });
292
+ }
293
+
294
+ async appendDecision(record) {
295
+ return this.#serialized(async () => {
296
+ this.#decisions.push(record);
297
+ await appendFile(this.files.decisions, `${JSON.stringify(record)}\n`, 'utf8');
298
+ return record;
299
+ });
300
+ }
301
+
302
+ async appendWake(record) {
303
+ return this.#serialized(async () => {
304
+ this.#wakes.push(record);
305
+ await appendFile(this.files.wakes, `${JSON.stringify(record)}\n`, 'utf8');
306
+ return record;
307
+ });
308
+ }
309
+
310
+ async putTrigger(definitionInput) {
311
+ return this.#serialized(async () => {
312
+ const definition = parseCompositeTriggerDefinition(definitionInput);
313
+ const key = this.#triggerKey(definition);
314
+ if (this.#triggers.has(key)) {
315
+ const error = new Error(
316
+ `Trigger ${definition.triggerId}@${definition.version} already exists`,
317
+ );
318
+ error.code = 'TRIGGER_ALREADY_EXISTS';
319
+ throw error;
320
+ }
321
+ this.#triggers.set(key, definition);
322
+ await appendFile(
323
+ this.files.triggers,
324
+ `${JSON.stringify(definition)}\n`,
325
+ 'utf8',
326
+ );
327
+ return definition;
328
+ });
329
+ }
330
+
331
+ listTriggers() {
332
+ return [...this.#triggers.values()];
333
+ }
334
+
335
+ getTriggerState(triggerId, version) {
336
+ return this.#triggerStates.get(`${triggerId}@${version}`) ?? {
337
+ triggerId,
338
+ version,
339
+ status: 'active',
340
+ updatedAt: null,
341
+ actor: null,
342
+ owner: null,
343
+ connectionIds: [],
344
+ fireCount: 0,
345
+ lastFiredAt: null,
346
+ revision: 0,
347
+ };
348
+ }
349
+
350
+ async setTriggerState(
351
+ triggerId,
352
+ version,
353
+ status,
354
+ actor = null,
355
+ metadata = {},
356
+ ) {
357
+ return this.#serialized(async () => {
358
+ const key = `${triggerId}@${version}`;
359
+ const previous = this.#triggerStates.get(key);
360
+ const allowedStatuses = new Set([
361
+ 'active',
362
+ 'paused',
363
+ 'completed',
364
+ 'expired',
365
+ 'deleted',
366
+ ]);
367
+ if (!allowedStatuses.has(status)) {
368
+ throw new Error(`Invalid trigger lifecycle status: ${status}`);
369
+ }
370
+ const next = {
371
+ triggerId: String(triggerId),
372
+ version: String(version),
373
+ status,
374
+ updatedAt: new Date().toISOString(),
375
+ actor,
376
+ owner: metadata.owner ?? previous?.owner ?? null,
377
+ connectionIds: Array.isArray(metadata.connectionIds)
378
+ ? metadata.connectionIds.map(String)
379
+ : previous?.connectionIds ?? [],
380
+ fireCount: Number.isInteger(metadata.fireCount)
381
+ ? metadata.fireCount
382
+ : previous?.fireCount ?? 0,
383
+ lastFiredAt: metadata.lastFiredAt !== undefined
384
+ ? metadata.lastFiredAt
385
+ : previous?.lastFiredAt ?? null,
386
+ revision: (previous?.revision ?? 0) + 1,
387
+ };
388
+ this.#triggerStates.set(`${next.triggerId}@${next.version}`, next);
389
+ await appendFile(
390
+ this.files.triggerStates,
391
+ `${JSON.stringify(next)}\n`,
392
+ 'utf8',
393
+ );
394
+ return next;
395
+ });
396
+ }
397
+
398
+ listTriggerStates() {
399
+ return [...this.#triggerStates.values()];
400
+ }
401
+
402
+ async putEventSource(input) {
403
+ return this.#serialized(async () => {
404
+ const source = {
405
+ sourceId: String(input.sourceId),
406
+ connectionId: String(input.connectionId),
407
+ serverId: String(input.serverId || input.connectionId),
408
+ eventName: String(input.eventName),
409
+ ...(input.description ? { description: String(input.description) } : {}),
410
+ delivery: Array.isArray(input.delivery) && input.delivery.length
411
+ ? input.delivery.map(String)
412
+ : ['webhook'],
413
+ ...(input.inputSchema && typeof input.inputSchema === 'object'
414
+ ? { inputSchema: input.inputSchema }
415
+ : {}),
416
+ ...(input.payloadSchema && typeof input.payloadSchema === 'object'
417
+ ? { payloadSchema: input.payloadSchema }
418
+ : {}),
419
+ enabled: input.enabled !== false,
420
+ experimental: input.experimental !== false,
421
+ ...(input.metadata && typeof input.metadata === 'object'
422
+ ? { metadata: input.metadata }
423
+ : {}),
424
+ updatedAt: new Date().toISOString(),
425
+ };
426
+ if (!source.sourceId || !source.connectionId || !source.eventName) {
427
+ throw new Error('event source requires sourceId, connectionId, eventName');
428
+ }
429
+ this.#eventSources.set(source.sourceId, source);
430
+ await appendFile(
431
+ this.files.eventSources,
432
+ `${JSON.stringify(source)}\n`,
433
+ 'utf8',
434
+ );
435
+ return source;
436
+ });
437
+ }
438
+
439
+ listEventSources({ connectionIds, enabledOnly = false } = {}) {
440
+ const ids = Array.isArray(connectionIds) && connectionIds.length
441
+ ? new Set(connectionIds.map(String))
442
+ : null;
443
+ return [...this.#eventSources.values()]
444
+ .filter((source) => !ids || ids.has(source.connectionId))
445
+ .filter((source) => !enabledOnly || source.enabled !== false);
446
+ }
447
+
448
+ async appendTriggerMatch(recordInput) {
449
+ return this.#serialized(async () => {
450
+ const record = parseTriggerMatchRecord(recordInput);
451
+ this.#triggerMatchHistory.push(record);
452
+ this.#triggerMatches.set(record.matchId, record);
453
+ await appendFile(
454
+ this.files.triggerMatches,
455
+ `${JSON.stringify(record)}\n`,
456
+ 'utf8',
457
+ );
458
+ return record;
459
+ });
460
+ }
461
+
462
+ listTriggerMatches(triggerId) {
463
+ return [...this.#triggerMatches.values()]
464
+ .filter((record) => !triggerId || record.triggerId === triggerId);
465
+ }
466
+
467
+ listTriggerMatchHistory(matchId) {
468
+ return this.#triggerMatchHistory
469
+ .filter((record) => !matchId || record.matchId === matchId);
470
+ }
471
+
472
+ async appendMcpOccurrence(serverId, eventInput) {
473
+ return this.#serialized(async () => {
474
+ const event = McpEventOccurrenceSchema.parse(eventInput);
475
+ const key = `${serverId}:${event.eventId}`;
476
+ const existing = this.#mcpOccurrences.find(
477
+ (record) =>
478
+ record.serverId === serverId &&
479
+ record.event.eventId === event.eventId,
480
+ );
481
+ if (this.#mcpOccurrenceKeys.has(key)) {
482
+ return {
483
+ accepted: false,
484
+ sequence: existing?.sequence ?? null,
485
+ event: existing?.event ?? event,
486
+ };
487
+ }
488
+
489
+ const sequence = this.latestMcpEventSequence() + 1;
490
+ const record = { sequence, serverId, event };
491
+ this.#mcpOccurrenceKeys.add(key);
492
+ this.#mcpOccurrences.push(record);
493
+ await appendFile(
494
+ this.files.mcpOccurrences,
495
+ `${JSON.stringify(record)}\n`,
496
+ 'utf8',
497
+ );
498
+ return { accepted: true, sequence, event };
499
+ });
500
+ }
501
+
502
+ latestMcpEventSequence() {
503
+ return this.#mcpOccurrences.at(-1)?.sequence ?? 0;
504
+ }
505
+
506
+ listMcpOccurrencesAfter(sequence) {
507
+ return this.#mcpOccurrences
508
+ .filter((record) => record.sequence > sequence)
509
+ .map((record) => ({
510
+ sequence: record.sequence,
511
+ serverId: record.serverId,
512
+ event: {
513
+ ...record.event,
514
+ data: { ...record.event.data },
515
+ },
516
+ }));
517
+ }
518
+
519
+ getMcpClientState(connectionId, eventName) {
520
+ return this.#mcpClientStates.get(
521
+ `${String(connectionId)}::${String(eventName)}`,
522
+ ) ?? null;
523
+ }
524
+
525
+ listMcpClientStates(connectionId) {
526
+ return [...this.#mcpClientStates.values()]
527
+ .filter((state) =>
528
+ !connectionId || state.connectionId === String(connectionId)
529
+ );
530
+ }
531
+
532
+ async putMcpClientState(input) {
533
+ return this.#serialized(async () => {
534
+ const connectionId = String(input.connectionId || '');
535
+ const eventName = String(input.eventName || '');
536
+ if (!connectionId || !eventName) {
537
+ throw new Error('MCP client state requires connectionId and eventName');
538
+ }
539
+ const previous = this.getMcpClientState(connectionId, eventName);
540
+ const state = {
541
+ connectionId,
542
+ serverId: String(input.serverId || previous?.serverId || connectionId),
543
+ eventName,
544
+ cursor: input.cursor === null || input.cursor === undefined
545
+ ? null
546
+ : String(input.cursor),
547
+ updatedAt: new Date().toISOString(),
548
+ ...(input.lastEventAt || previous?.lastEventAt
549
+ ? { lastEventAt: String(input.lastEventAt || previous.lastEventAt) }
550
+ : {}),
551
+ ...(input.lastError
552
+ ? { lastError: String(input.lastError) }
553
+ : {}),
554
+ };
555
+ this.#mcpClientStates.set(
556
+ `${connectionId}::${eventName}`,
557
+ state,
558
+ );
559
+ await appendFile(
560
+ this.files.mcpClientStates,
561
+ `${JSON.stringify(state)}\n`,
562
+ 'utf8',
563
+ );
564
+ return state;
565
+ });
566
+ }
567
+
568
+ getTemporalDeadline(deadlineId) {
569
+ return this.#temporalDeadlines.get(String(deadlineId)) ?? null;
570
+ }
571
+
572
+ listTemporalDeadlines({ triggerId, matchId, status } = {}) {
573
+ return [...this.#temporalDeadlines.values()]
574
+ .filter((deadline) => !triggerId || deadline.triggerId === String(triggerId))
575
+ .filter((deadline) => !matchId || deadline.matchId === String(matchId))
576
+ .filter((deadline) => !status || deadline.status === String(status));
577
+ }
578
+
579
+ listDueTemporalDeadlines(nowIso) {
580
+ const now = Date.parse(nowIso);
581
+ return this.listTemporalDeadlines({ status: 'pending' })
582
+ .filter((deadline) => Date.parse(deadline.dueAt) <= now)
583
+ .sort((a, b) => Date.parse(a.dueAt) - Date.parse(b.dueAt));
584
+ }
585
+
586
+ async putTemporalDeadline(input) {
587
+ return this.#serialized(async () => {
588
+ const deadlineId = String(input.deadlineId || '');
589
+ if (!deadlineId || !input.triggerId || !input.matchId || !input.conditionId) {
590
+ throw new Error('Temporal deadline requires deadlineId, triggerId, matchId and conditionId');
591
+ }
592
+ const existing = this.#temporalDeadlines.get(deadlineId);
593
+ const now = new Date().toISOString();
594
+ const deadline = {
595
+ deadlineId,
596
+ triggerId: String(input.triggerId),
597
+ triggerVersion: String(input.triggerVersion || '1'),
598
+ matchId: String(input.matchId),
599
+ conditionId: String(input.conditionId),
600
+ dueAt: String(input.dueAt),
601
+ status: ['pending', 'cancelled', 'fired'].includes(input.status)
602
+ ? input.status
603
+ : existing?.status || 'pending',
604
+ createdAt: existing?.createdAt || now,
605
+ updatedAt: now,
606
+ };
607
+ if (!Number.isFinite(Date.parse(deadline.dueAt))) {
608
+ throw new Error('Temporal deadline dueAt must be an ISO timestamp');
609
+ }
610
+ this.#temporalDeadlines.set(deadlineId, deadline);
611
+ await appendFile(
612
+ this.files.temporalDeadlines,
613
+ `${JSON.stringify(deadline)}\n`,
614
+ 'utf8',
615
+ );
616
+ return deadline;
617
+ });
618
+ }
619
+
620
+ async setTemporalDeadlineStatus(deadlineId, status) {
621
+ const existing = this.getTemporalDeadline(deadlineId);
622
+ if (!existing) return null;
623
+ return this.putTemporalDeadline({
624
+ ...existing,
625
+ status,
626
+ });
627
+ }
628
+
629
+ getDerivedContract(eventName, contractVersion) {
630
+ return this.#derivedContracts.get(
631
+ `${String(eventName)}@${String(contractVersion)}`,
632
+ ) ?? null;
633
+ }
634
+
635
+ listDerivedContracts(eventName) {
636
+ return [...this.#derivedContracts.values()]
637
+ .filter((record) => !eventName || record.eventName === String(eventName))
638
+ .sort((a, b) =>
639
+ a.eventName.localeCompare(b.eventName) ||
640
+ a.contractVersion.localeCompare(b.contractVersion, undefined, { numeric: true })
641
+ );
642
+ }
643
+
644
+ async putDerivedContract(recordInput) {
645
+ return this.#serialized(async () => {
646
+ const key =
647
+ `${String(recordInput.eventName)}@${String(recordInput.contractVersion)}`;
648
+ const existing = this.#derivedContracts.get(key);
649
+ const record = {
650
+ eventName: String(recordInput.eventName),
651
+ contractVersion: String(recordInput.contractVersion),
652
+ payloadSchema:
653
+ recordInput.payloadSchema && typeof recordInput.payloadSchema === 'object'
654
+ ? recordInput.payloadSchema
655
+ : {},
656
+ schemaFingerprint: String(recordInput.schemaFingerprint),
657
+ producers: Array.isArray(recordInput.producers)
658
+ ? recordInput.producers.map((producer) => ({
659
+ triggerId: String(producer.triggerId),
660
+ triggerVersion: String(producer.triggerVersion),
661
+ }))
662
+ : existing?.producers ?? [],
663
+ createdAt: existing?.createdAt ?? new Date().toISOString(),
664
+ updatedAt: new Date().toISOString(),
665
+ };
666
+ this.#derivedContracts.set(key, record);
667
+ await appendFile(
668
+ this.files.derivedContracts,
669
+ `${JSON.stringify(record)}\n`,
670
+ 'utf8',
671
+ );
672
+ return record;
673
+ });
674
+ }
675
+
676
+ getDerivedEvent(sourceEventId, serverId = 'event-intelligence:derived') {
677
+ return this.#derivedEvents.get(
678
+ `${String(serverId)}::${String(sourceEventId)}`,
679
+ ) ?? null;
680
+ }
681
+
682
+ listDerivedEvents({ triggerId, matchId, name } = {}) {
683
+ return [...this.#derivedEvents.values()]
684
+ .filter((record) => !triggerId || record.triggerId === String(triggerId))
685
+ .filter((record) => !matchId || record.matchId === String(matchId))
686
+ .filter((record) => !name || record.event.name === String(name));
687
+ }
688
+
689
+ async appendDerivedEvent(recordInput) {
690
+ return this.#serialized(async () => {
691
+ const record = parseDerivedEventRecord(recordInput);
692
+ const key =
693
+ `${record.event.serverId ?? ''}::${record.event.sourceEventId}`;
694
+ const existing = this.#derivedEvents.get(key);
695
+ if (existing) {
696
+ return {
697
+ accepted: false,
698
+ record: existing,
699
+ };
700
+ }
701
+ this.#derivedEvents.set(key, record);
702
+ await appendFile(
703
+ this.files.derivedEvents,
704
+ `${JSON.stringify(record)}\n`,
705
+ 'utf8',
706
+ );
707
+ return {
708
+ accepted: true,
709
+ record,
710
+ };
711
+ });
712
+ }
713
+
714
+ async appendAudit(input) {
715
+ return this.#serialized(async () => {
716
+ const record = await this.#auditChain.append(input);
717
+ await appendFile(this.files.audit, `${JSON.stringify(record)}\n`, 'utf8');
718
+ return record;
719
+ });
720
+ }
721
+
722
+ listAudit({ traceId, afterSequence = -1 } = {}) {
723
+ return this.#auditChain
724
+ .list()
725
+ .filter((record) => record.sequence > afterSequence)
726
+ .filter((record) => !traceId || record.traceId === traceId);
727
+ }
728
+
729
+ trace(traceId) {
730
+ return {
731
+ events: this.#events.filter((record) => record.lineage?.traceId === traceId),
732
+ decisions: this.#decisions.filter((record) => record.traceId === traceId),
733
+ wakes: this.#wakes.filter((record) => record.traceId === traceId),
734
+ triggerMatches: this.listTriggerMatches().filter((record) =>
735
+ record.sourceEvents.some((event) => event.traceId === traceId),
736
+ ),
737
+ audit: this.listAudit({ traceId }),
738
+ };
739
+ }
740
+
741
+ latestWake(wakeId) {
742
+ return [...this.#wakes].reverse().find((wake) => wake.wakeId === wakeId) ?? null;
743
+ }
744
+
745
+ auditLength() {
746
+ return this.#auditChain.list().length;
747
+ }
748
+
749
+ async verifyAudit() {
750
+ return this.#auditChain.verify();
751
+ }
752
+
753
+ #eventKey(lineage) {
754
+ return `${lineage.environmentId}:${lineage.subscriptionId}:${lineage.sourceEventId}`;
755
+ }
756
+
757
+ #triggerKey(definition) {
758
+ return `${definition.triggerId}@${definition.version}`;
759
+ }
760
+
761
+ #serialized(operation) {
762
+ const task = this.#queue.then(operation);
763
+ this.#queue = task.then(
764
+ () => undefined,
765
+ () => undefined,
766
+ );
767
+ return task;
768
+ }
769
+ }