mcp-event-intelligence 0.1.1 → 0.2.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.
- package/CHANGELOG.md +57 -0
- package/README.md +67 -4
- package/bin/mcp-event-intelligence.mjs +1 -1
- package/dist/src/composite/derivedGraph.d.ts +4 -0
- package/dist/src/composite/derivedGraph.js +46 -0
- package/dist/src/composite/engine.d.ts +3 -64
- package/dist/src/composite/engine.js +26 -343
- package/dist/src/composite/matching.d.ts +8 -0
- package/dist/src/composite/matching.js +109 -0
- package/dist/src/composite/runtimeSupport.d.ts +24 -0
- package/dist/src/composite/runtimeSupport.js +176 -0
- package/dist/src/composite/store.d.ts +59 -0
- package/dist/src/composite/store.js +1 -0
- package/dist/src/intelligenceProtocol/schemas.d.ts +9 -7
- package/dist/src/intelligenceProtocol/schemas.js +1 -0
- package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +6 -6
- package/dist/src/mcpEvents/erpnextProvider.d.ts +20 -0
- package/dist/src/mcpEvents/erpnextProvider.js +80 -0
- package/package.json +5 -1
- package/scripts/host-integration.d.mts +34 -4
- package/scripts/host-integration.mjs +44 -1
- package/scripts/lib/composite-wake-coordinator.mjs +233 -9
- package/scripts/lib/event-processor.mjs +235 -20
- package/scripts/lib/local-event-intelligence-runtime.mjs +227 -82
- package/scripts/lib/mcp-events-client.mjs +173 -66
- package/scripts/lib/persistent-event-store.mjs +308 -1
- package/scripts/lib/wake-retry-scheduler.mjs +94 -0
- package/scripts/mcp-stdio-server.mjs +1 -1
- package/scripts/service.mjs +20 -0
- package/server.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { appendFile, mkdir, readFile } from 'node:fs/promises';
|
|
1
|
+
import { appendFile, mkdir, readFile, readdir } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import {
|
|
4
4
|
AuditChain,
|
|
@@ -9,6 +9,29 @@ import {
|
|
|
9
9
|
McpEventOccurrenceSchema,
|
|
10
10
|
} from '../../dist/src/intelligenceProtocol/index.js';
|
|
11
11
|
|
|
12
|
+
export const DEFAULT_EVENT_SCOPE_ID = 'default';
|
|
13
|
+
|
|
14
|
+
export function normalizeEventScopeId(input = DEFAULT_EVENT_SCOPE_ID) {
|
|
15
|
+
const scopeId = String(input ?? DEFAULT_EVENT_SCOPE_ID).trim();
|
|
16
|
+
if (!scopeId) throw new Error('Event Intelligence scopeId must not be empty');
|
|
17
|
+
if (scopeId.length > 240) {
|
|
18
|
+
throw new Error('Event Intelligence scopeId must be at most 240 characters');
|
|
19
|
+
}
|
|
20
|
+
return scopeId;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function scopeDirectoryName(scopeId) {
|
|
24
|
+
return Buffer.from(scopeId, 'utf8').toString('base64url');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function scopeIdFromDirectoryName(name) {
|
|
28
|
+
try {
|
|
29
|
+
return Buffer.from(name, 'base64url').toString('utf8');
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
12
35
|
async function readJsonLines(filePath) {
|
|
13
36
|
try {
|
|
14
37
|
const raw = await readFile(filePath, 'utf8');
|
|
@@ -28,6 +51,7 @@ export class PersistentEventStore {
|
|
|
28
51
|
#events = [];
|
|
29
52
|
#decisions = [];
|
|
30
53
|
#wakes = [];
|
|
54
|
+
#wakeDeliveries = new Map();
|
|
31
55
|
#triggers = new Map();
|
|
32
56
|
#triggerStates = new Map();
|
|
33
57
|
#eventSources = new Map();
|
|
@@ -40,6 +64,7 @@ export class PersistentEventStore {
|
|
|
40
64
|
#derivedEvents = new Map();
|
|
41
65
|
#derivedContracts = new Map();
|
|
42
66
|
#auditChain = new AuditChain();
|
|
67
|
+
#scopeStores = new Map();
|
|
43
68
|
|
|
44
69
|
constructor(dataDir) {
|
|
45
70
|
this.dataDir = dataDir;
|
|
@@ -47,6 +72,7 @@ export class PersistentEventStore {
|
|
|
47
72
|
events: path.join(dataDir, 'events.jsonl'),
|
|
48
73
|
decisions: path.join(dataDir, 'decisions.jsonl'),
|
|
49
74
|
wakes: path.join(dataDir, 'wakes.jsonl'),
|
|
75
|
+
wakeDeliveries: path.join(dataDir, 'wake-deliveries.jsonl'),
|
|
50
76
|
triggers: path.join(dataDir, 'triggers.jsonl'),
|
|
51
77
|
triggerStates: path.join(dataDir, 'trigger-states.jsonl'),
|
|
52
78
|
eventSources: path.join(dataDir, 'event-sources.jsonl'),
|
|
@@ -60,6 +86,41 @@ export class PersistentEventStore {
|
|
|
60
86
|
};
|
|
61
87
|
}
|
|
62
88
|
|
|
89
|
+
async forScope(scopeIdInput = DEFAULT_EVENT_SCOPE_ID) {
|
|
90
|
+
const scopeId = normalizeEventScopeId(scopeIdInput);
|
|
91
|
+
if (scopeId === DEFAULT_EVENT_SCOPE_ID) return this;
|
|
92
|
+
|
|
93
|
+
const existing = this.#scopeStores.get(scopeId);
|
|
94
|
+
if (existing) return existing;
|
|
95
|
+
|
|
96
|
+
const scoped = new PersistentEventStore(
|
|
97
|
+
path.join(this.dataDir, 'scopes', scopeDirectoryName(scopeId)),
|
|
98
|
+
);
|
|
99
|
+
await scoped.init();
|
|
100
|
+
this.#scopeStores.set(scopeId, scoped);
|
|
101
|
+
return scoped;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async listScopeIds() {
|
|
105
|
+
const scopes = new Set([
|
|
106
|
+
DEFAULT_EVENT_SCOPE_ID,
|
|
107
|
+
...this.#scopeStores.keys(),
|
|
108
|
+
]);
|
|
109
|
+
try {
|
|
110
|
+
const entries = await readdir(path.join(this.dataDir, 'scopes'), {
|
|
111
|
+
withFileTypes: true,
|
|
112
|
+
});
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
if (!entry.isDirectory()) continue;
|
|
115
|
+
const scopeId = scopeIdFromDirectoryName(entry.name);
|
|
116
|
+
if (scopeId) scopes.add(scopeId);
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
120
|
+
}
|
|
121
|
+
return [...scopes].sort();
|
|
122
|
+
}
|
|
123
|
+
|
|
63
124
|
async init() {
|
|
64
125
|
await mkdir(this.dataDir, { recursive: true });
|
|
65
126
|
|
|
@@ -67,6 +128,7 @@ export class PersistentEventStore {
|
|
|
67
128
|
events,
|
|
68
129
|
decisions,
|
|
69
130
|
wakes,
|
|
131
|
+
wakeDeliveries,
|
|
70
132
|
triggers,
|
|
71
133
|
triggerStates,
|
|
72
134
|
eventSources,
|
|
@@ -81,6 +143,7 @@ export class PersistentEventStore {
|
|
|
81
143
|
readJsonLines(this.files.events),
|
|
82
144
|
readJsonLines(this.files.decisions),
|
|
83
145
|
readJsonLines(this.files.wakes),
|
|
146
|
+
readJsonLines(this.files.wakeDeliveries),
|
|
84
147
|
readJsonLines(this.files.triggers),
|
|
85
148
|
readJsonLines(this.files.triggerStates),
|
|
86
149
|
readJsonLines(this.files.eventSources),
|
|
@@ -97,6 +160,42 @@ export class PersistentEventStore {
|
|
|
97
160
|
this.#decisions = decisions;
|
|
98
161
|
this.#wakes = wakes;
|
|
99
162
|
|
|
163
|
+
this.#wakeDeliveries = new Map();
|
|
164
|
+
for (const raw of wakeDeliveries) {
|
|
165
|
+
if (!raw?.wakeId || !raw?.runtime || (!raw?.sourceId && !raw?.matchId)) continue;
|
|
166
|
+
const status = [
|
|
167
|
+
'pending',
|
|
168
|
+
'claimed',
|
|
169
|
+
'retry_pending',
|
|
170
|
+
'delivered',
|
|
171
|
+
'dead_letter',
|
|
172
|
+
].includes(raw.status)
|
|
173
|
+
? raw.status
|
|
174
|
+
: 'pending';
|
|
175
|
+
const record = {
|
|
176
|
+
wakeId: String(raw.wakeId),
|
|
177
|
+
sourceType: raw.sourceType === 'event' ? 'event' : 'composite',
|
|
178
|
+
sourceId: String(raw.sourceId || raw.matchId),
|
|
179
|
+
matchId: raw.matchId ? String(raw.matchId) : '',
|
|
180
|
+
triggerId: String(raw.triggerId || ''),
|
|
181
|
+
triggerVersion: String(raw.triggerVersion || '1'),
|
|
182
|
+
runtime: String(raw.runtime),
|
|
183
|
+
status,
|
|
184
|
+
attemptCount:
|
|
185
|
+
Number.isInteger(raw.attemptCount) && raw.attemptCount >= 0
|
|
186
|
+
? raw.attemptCount
|
|
187
|
+
: 0,
|
|
188
|
+
nextAttemptAt: String(raw.nextAttemptAt || raw.updatedAt || new Date(0).toISOString()),
|
|
189
|
+
leaseOwner: raw.leaseOwner ? String(raw.leaseOwner) : null,
|
|
190
|
+
leaseUntil: raw.leaseUntil ? String(raw.leaseUntil) : null,
|
|
191
|
+
runtimeReceiptId: raw.runtimeReceiptId ? String(raw.runtimeReceiptId) : null,
|
|
192
|
+
lastError: raw.lastError ? String(raw.lastError) : null,
|
|
193
|
+
createdAt: String(raw.createdAt || new Date(0).toISOString()),
|
|
194
|
+
updatedAt: String(raw.updatedAt || new Date(0).toISOString()),
|
|
195
|
+
};
|
|
196
|
+
this.#wakeDeliveries.set(record.wakeId, record);
|
|
197
|
+
}
|
|
198
|
+
|
|
100
199
|
this.#triggers = new Map();
|
|
101
200
|
for (const raw of triggers) {
|
|
102
201
|
const definition = parseCompositeTriggerDefinition(raw);
|
|
@@ -265,6 +364,7 @@ export class PersistentEventStore {
|
|
|
265
364
|
events: events.length,
|
|
266
365
|
decisions: decisions.length,
|
|
267
366
|
wakes: wakes.length,
|
|
367
|
+
wakeDeliveries: this.#wakeDeliveries.size,
|
|
268
368
|
triggers: this.#triggers.size,
|
|
269
369
|
triggerStates: this.#triggerStates.size,
|
|
270
370
|
eventSources: this.#eventSources.size,
|
|
@@ -307,6 +407,213 @@ export class PersistentEventStore {
|
|
|
307
407
|
});
|
|
308
408
|
}
|
|
309
409
|
|
|
410
|
+
getWakeDelivery(wakeId) {
|
|
411
|
+
return this.#wakeDeliveries.get(String(wakeId)) ?? null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
listDueWakeDeliveries(nowIso = new Date().toISOString()) {
|
|
415
|
+
const now = Date.parse(nowIso);
|
|
416
|
+
return [...this.#wakeDeliveries.values()]
|
|
417
|
+
.filter((record) => {
|
|
418
|
+
if (record.status === 'pending' || record.status === 'retry_pending') {
|
|
419
|
+
return Date.parse(record.nextAttemptAt) <= now;
|
|
420
|
+
}
|
|
421
|
+
if (record.status === 'claimed' && record.leaseUntil) {
|
|
422
|
+
return Date.parse(record.leaseUntil) <= now;
|
|
423
|
+
}
|
|
424
|
+
if (record.status === 'delivered') {
|
|
425
|
+
const latestWake = this.latestWake(record.wakeId);
|
|
426
|
+
const wakeFinalized =
|
|
427
|
+
latestWake?.status === 'delivered' ||
|
|
428
|
+
latestWake?.status === 'handled';
|
|
429
|
+
if (record.sourceType === 'event') {
|
|
430
|
+
return !wakeFinalized;
|
|
431
|
+
}
|
|
432
|
+
const match = this.#triggerMatches.get(record.matchId);
|
|
433
|
+
return !wakeFinalized || match?.status !== 'fired';
|
|
434
|
+
}
|
|
435
|
+
return false;
|
|
436
|
+
})
|
|
437
|
+
.sort((a, b) =>
|
|
438
|
+
Date.parse(a.nextAttemptAt) - Date.parse(b.nextAttemptAt) ||
|
|
439
|
+
a.wakeId.localeCompare(b.wakeId)
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async ensureWakeDelivery(input) {
|
|
444
|
+
return this.#serialized(async () => {
|
|
445
|
+
const wakeId = String(input.wakeId || '');
|
|
446
|
+
const sourceType = input.sourceType === 'event' ? 'event' : 'composite';
|
|
447
|
+
const matchId = String(input.matchId || '');
|
|
448
|
+
const sourceId = String(input.sourceId || matchId || '');
|
|
449
|
+
const runtime = String(input.runtime || '');
|
|
450
|
+
if (!wakeId || !sourceId || !runtime) {
|
|
451
|
+
throw new Error('Wake delivery requires wakeId, sourceId and runtime');
|
|
452
|
+
}
|
|
453
|
+
if (sourceType === 'composite' && !matchId) {
|
|
454
|
+
throw new Error('Composite wake delivery requires matchId');
|
|
455
|
+
}
|
|
456
|
+
const existing = this.#wakeDeliveries.get(wakeId);
|
|
457
|
+
if (existing) return existing;
|
|
458
|
+
|
|
459
|
+
const now = String(input.now ?? new Date().toISOString());
|
|
460
|
+
const record = {
|
|
461
|
+
wakeId,
|
|
462
|
+
sourceType,
|
|
463
|
+
sourceId,
|
|
464
|
+
matchId,
|
|
465
|
+
triggerId: String(input.triggerId || ''),
|
|
466
|
+
triggerVersion: String(input.triggerVersion || '1'),
|
|
467
|
+
runtime,
|
|
468
|
+
status: 'pending',
|
|
469
|
+
attemptCount: 0,
|
|
470
|
+
nextAttemptAt: now,
|
|
471
|
+
leaseOwner: null,
|
|
472
|
+
leaseUntil: null,
|
|
473
|
+
runtimeReceiptId: null,
|
|
474
|
+
lastError: null,
|
|
475
|
+
createdAt: now,
|
|
476
|
+
updatedAt: now,
|
|
477
|
+
};
|
|
478
|
+
this.#wakeDeliveries.set(wakeId, record);
|
|
479
|
+
await appendFile(
|
|
480
|
+
this.files.wakeDeliveries,
|
|
481
|
+
`${JSON.stringify(record)}\n`,
|
|
482
|
+
'utf8',
|
|
483
|
+
);
|
|
484
|
+
return record;
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async claimWakeDelivery(wakeIdInput, {
|
|
489
|
+
workerId,
|
|
490
|
+
now = new Date().toISOString(),
|
|
491
|
+
leaseMs = 30000,
|
|
492
|
+
} = {}) {
|
|
493
|
+
return this.#serialized(async () => {
|
|
494
|
+
const wakeId = String(wakeIdInput || '');
|
|
495
|
+
const owner = String(workerId || '');
|
|
496
|
+
if (!wakeId || !owner) {
|
|
497
|
+
throw new Error('Wake delivery claim requires wakeId and workerId');
|
|
498
|
+
}
|
|
499
|
+
const current = this.#wakeDeliveries.get(wakeId);
|
|
500
|
+
if (!current) return null;
|
|
501
|
+
if (current.status === 'delivered' || current.status === 'dead_letter') {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const nowMs = Date.parse(now);
|
|
506
|
+
const ready =
|
|
507
|
+
(
|
|
508
|
+
(current.status === 'pending' || current.status === 'retry_pending') &&
|
|
509
|
+
Date.parse(current.nextAttemptAt) <= nowMs
|
|
510
|
+
) ||
|
|
511
|
+
(
|
|
512
|
+
current.status === 'claimed' &&
|
|
513
|
+
current.leaseUntil &&
|
|
514
|
+
Date.parse(current.leaseUntil) <= nowMs
|
|
515
|
+
);
|
|
516
|
+
if (!ready) return null;
|
|
517
|
+
|
|
518
|
+
const record = {
|
|
519
|
+
...current,
|
|
520
|
+
status: 'claimed',
|
|
521
|
+
attemptCount: current.attemptCount + 1,
|
|
522
|
+
leaseOwner: owner,
|
|
523
|
+
leaseUntil: new Date(nowMs + Math.max(1000, Number(leaseMs) || 30000)).toISOString(),
|
|
524
|
+
updatedAt: now,
|
|
525
|
+
};
|
|
526
|
+
this.#wakeDeliveries.set(wakeId, record);
|
|
527
|
+
await appendFile(
|
|
528
|
+
this.files.wakeDeliveries,
|
|
529
|
+
`${JSON.stringify(record)}\n`,
|
|
530
|
+
'utf8',
|
|
531
|
+
);
|
|
532
|
+
return record;
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async completeWakeDelivery(wakeIdInput, {
|
|
537
|
+
workerId,
|
|
538
|
+
runtimeReceiptId,
|
|
539
|
+
now = new Date().toISOString(),
|
|
540
|
+
} = {}) {
|
|
541
|
+
return this.#serialized(async () => {
|
|
542
|
+
const wakeId = String(wakeIdInput || '');
|
|
543
|
+
const current = this.#wakeDeliveries.get(wakeId);
|
|
544
|
+
if (!current) return null;
|
|
545
|
+
if (current.status === 'delivered') return current;
|
|
546
|
+
if (
|
|
547
|
+
current.status !== 'claimed' ||
|
|
548
|
+
current.leaseOwner !== String(workerId || '')
|
|
549
|
+
) {
|
|
550
|
+
const error = new Error(`Wake delivery ${wakeId} is not owned by this worker`);
|
|
551
|
+
error.code = 'WAKE_DELIVERY_CLAIM_LOST';
|
|
552
|
+
throw error;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const record = {
|
|
556
|
+
...current,
|
|
557
|
+
status: 'delivered',
|
|
558
|
+
runtimeReceiptId: String(runtimeReceiptId || ''),
|
|
559
|
+
leaseOwner: null,
|
|
560
|
+
leaseUntil: null,
|
|
561
|
+
nextAttemptAt: now,
|
|
562
|
+
lastError: null,
|
|
563
|
+
updatedAt: now,
|
|
564
|
+
};
|
|
565
|
+
this.#wakeDeliveries.set(wakeId, record);
|
|
566
|
+
await appendFile(
|
|
567
|
+
this.files.wakeDeliveries,
|
|
568
|
+
`${JSON.stringify(record)}\n`,
|
|
569
|
+
'utf8',
|
|
570
|
+
);
|
|
571
|
+
return record;
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async failWakeDelivery(wakeIdInput, {
|
|
576
|
+
workerId,
|
|
577
|
+
error,
|
|
578
|
+
now = new Date().toISOString(),
|
|
579
|
+
maxAttempts = 5,
|
|
580
|
+
nextAttemptAt = now,
|
|
581
|
+
} = {}) {
|
|
582
|
+
return this.#serialized(async () => {
|
|
583
|
+
const wakeId = String(wakeIdInput || '');
|
|
584
|
+
const current = this.#wakeDeliveries.get(wakeId);
|
|
585
|
+
if (!current) return null;
|
|
586
|
+
if (
|
|
587
|
+
current.status !== 'claimed' ||
|
|
588
|
+
current.leaseOwner !== String(workerId || '')
|
|
589
|
+
) {
|
|
590
|
+
const claimError = new Error(
|
|
591
|
+
`Wake delivery ${wakeId} is not owned by this worker`,
|
|
592
|
+
);
|
|
593
|
+
claimError.code = 'WAKE_DELIVERY_CLAIM_LOST';
|
|
594
|
+
throw claimError;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const terminal = current.attemptCount >= Math.max(1, Number(maxAttempts) || 5);
|
|
598
|
+
const record = {
|
|
599
|
+
...current,
|
|
600
|
+
status: terminal ? 'dead_letter' : 'retry_pending',
|
|
601
|
+
leaseOwner: null,
|
|
602
|
+
leaseUntil: null,
|
|
603
|
+
nextAttemptAt: terminal ? now : String(nextAttemptAt),
|
|
604
|
+
lastError: error instanceof Error ? error.message : String(error || 'wake delivery failed'),
|
|
605
|
+
updatedAt: now,
|
|
606
|
+
};
|
|
607
|
+
this.#wakeDeliveries.set(wakeId, record);
|
|
608
|
+
await appendFile(
|
|
609
|
+
this.files.wakeDeliveries,
|
|
610
|
+
`${JSON.stringify(record)}\n`,
|
|
611
|
+
'utf8',
|
|
612
|
+
);
|
|
613
|
+
return record;
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
310
617
|
async putTrigger(definitionInput) {
|
|
311
618
|
return this.#serialized(async () => {
|
|
312
619
|
const definition = parseCompositeTriggerDefinition(definitionInput);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export class WakeRetryScheduler {
|
|
2
|
+
constructor({
|
|
3
|
+
store,
|
|
4
|
+
resolveCoordinator,
|
|
5
|
+
eventProcessor = null,
|
|
6
|
+
intervalMs = 1000,
|
|
7
|
+
now = () => new Date(),
|
|
8
|
+
}) {
|
|
9
|
+
this.store = store;
|
|
10
|
+
this.resolveCoordinator = resolveCoordinator;
|
|
11
|
+
this.eventProcessor = eventProcessor;
|
|
12
|
+
this.intervalMs = Math.max(100, Number(intervalMs) || 1000);
|
|
13
|
+
this.now = now;
|
|
14
|
+
this.timer = null;
|
|
15
|
+
this.running = false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async runDue() {
|
|
19
|
+
if (this.running) return [];
|
|
20
|
+
this.running = true;
|
|
21
|
+
try {
|
|
22
|
+
const due = this.store.listDueWakeDeliveries(this.now().toISOString());
|
|
23
|
+
const outcomes = [];
|
|
24
|
+
|
|
25
|
+
for (const delivery of due) {
|
|
26
|
+
if (delivery.sourceType === 'event') {
|
|
27
|
+
if (!this.eventProcessor || typeof this.eventProcessor.retryWake !== 'function') {
|
|
28
|
+
outcomes.push({
|
|
29
|
+
wakeId: delivery.wakeId,
|
|
30
|
+
status: 'event_processor_unconfigured',
|
|
31
|
+
});
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const result = await this.eventProcessor.retryWake(delivery.wakeId);
|
|
35
|
+
outcomes.push({
|
|
36
|
+
wakeId: delivery.wakeId,
|
|
37
|
+
status: result.status,
|
|
38
|
+
});
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const coordinator = this.resolveCoordinator(delivery.runtime);
|
|
43
|
+
if (!coordinator) {
|
|
44
|
+
outcomes.push({
|
|
45
|
+
wakeId: delivery.wakeId,
|
|
46
|
+
status: 'runtime_unconfigured',
|
|
47
|
+
});
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const match = this.store.listTriggerMatches(delivery.triggerId)
|
|
52
|
+
.filter((candidate) => candidate.matchId === delivery.matchId)
|
|
53
|
+
.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0];
|
|
54
|
+
|
|
55
|
+
if (!match) {
|
|
56
|
+
outcomes.push({
|
|
57
|
+
wakeId: delivery.wakeId,
|
|
58
|
+
status: 'match_missing',
|
|
59
|
+
});
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const result = await coordinator.deliverMatched(match);
|
|
64
|
+
outcomes.push({
|
|
65
|
+
wakeId: delivery.wakeId,
|
|
66
|
+
status: result.status,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return outcomes;
|
|
71
|
+
} finally {
|
|
72
|
+
this.running = false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
start() {
|
|
77
|
+
if (this.timer) return;
|
|
78
|
+
this.timer = setInterval(() => {
|
|
79
|
+
this.runDue().catch((error) => {
|
|
80
|
+
console.error(JSON.stringify({
|
|
81
|
+
message: 'wake_retry_scheduler_error',
|
|
82
|
+
error: error instanceof Error ? error.message : String(error),
|
|
83
|
+
}));
|
|
84
|
+
});
|
|
85
|
+
}, this.intervalMs);
|
|
86
|
+
this.timer.unref?.();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
stop() {
|
|
90
|
+
if (!this.timer) return;
|
|
91
|
+
clearInterval(this.timer);
|
|
92
|
+
this.timer = null;
|
|
93
|
+
}
|
|
94
|
+
}
|
package/scripts/service.mjs
CHANGED
|
@@ -49,6 +49,9 @@ import {
|
|
|
49
49
|
import {
|
|
50
50
|
TemporalDeadlineScheduler,
|
|
51
51
|
} from './lib/temporal-deadline-scheduler.mjs';
|
|
52
|
+
import {
|
|
53
|
+
WakeRetryScheduler,
|
|
54
|
+
} from './lib/wake-retry-scheduler.mjs';
|
|
52
55
|
|
|
53
56
|
const port = Number(process.env.PORT ?? 3000);
|
|
54
57
|
const dataDir = process.env.DATA_DIR ?? './data';
|
|
@@ -66,10 +69,18 @@ const wakeDeliverer = createHttpWakeDeliverer(
|
|
|
66
69
|
process.env.RUNTIME_WAKE_URL ?? '',
|
|
67
70
|
);
|
|
68
71
|
|
|
72
|
+
const deliveryOptions = {
|
|
73
|
+
leaseMs: Number(process.env.WAKE_DELIVERY_LEASE_MS ?? 30000),
|
|
74
|
+
maxAttempts: Number(process.env.WAKE_DELIVERY_MAX_ATTEMPTS ?? 5),
|
|
75
|
+
retryBaseDelayMs: Number(process.env.WAKE_RETRY_BASE_DELAY_MS ?? 1000),
|
|
76
|
+
retryMaxDelayMs: Number(process.env.WAKE_RETRY_MAX_DELAY_MS ?? 60000),
|
|
77
|
+
};
|
|
78
|
+
|
|
69
79
|
const processor = new EventProcessor({
|
|
70
80
|
store,
|
|
71
81
|
evaluator,
|
|
72
82
|
wakeDeliverer,
|
|
83
|
+
...deliveryOptions,
|
|
73
84
|
});
|
|
74
85
|
const compositeTriggers = new CompositeTriggerEngine(store, evaluator);
|
|
75
86
|
const runtimeWakeTargets = readRuntimeWakeTargets(
|
|
@@ -83,6 +94,7 @@ const wakeCoordinators = new Map(
|
|
|
83
94
|
triggerEngine: compositeTriggers,
|
|
84
95
|
deliverer: createSignedRuntimeWakeDeliverer(target),
|
|
85
96
|
packetBuilder: buildGenericRuntimeWakePacket,
|
|
97
|
+
...deliveryOptions,
|
|
86
98
|
}),
|
|
87
99
|
]),
|
|
88
100
|
);
|
|
@@ -102,6 +114,14 @@ const temporalScheduler = new TemporalDeadlineScheduler({
|
|
|
102
114
|
});
|
|
103
115
|
temporalScheduler.start();
|
|
104
116
|
|
|
117
|
+
const wakeRetryScheduler = new WakeRetryScheduler({
|
|
118
|
+
store,
|
|
119
|
+
eventProcessor: processor,
|
|
120
|
+
resolveCoordinator: (runtime) => wakeCoordinators.get(runtime) ?? null,
|
|
121
|
+
intervalMs: Number(process.env.WAKE_RETRY_TICK_MS ?? 1000),
|
|
122
|
+
});
|
|
123
|
+
wakeRetryScheduler.start();
|
|
124
|
+
|
|
105
125
|
const triggerControl = new TriggerControlPlane({
|
|
106
126
|
store,
|
|
107
127
|
triggerEngine: compositeTriggers,
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.sarooo17/event-intelligence",
|
|
4
4
|
"title": "MCP Event Intelligence",
|
|
5
5
|
"description": "Durable temporal event intelligence: composite triggers, derived events, and runtime wakeups.",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.2.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/sarooo17/event-intelligence",
|
|
9
9
|
"source": "github"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "mcp-event-intelligence",
|
|
16
|
-
"version": "0.
|
|
16
|
+
"version": "0.2.0",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
},
|