mcp-event-intelligence 0.1.0 → 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 +73 -0
- package/README.md +110 -5
- package/bin/mcp-event-intelligence.mjs +5 -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/dist/src/mcpEvents/provider.d.ts +48 -0
- package/dist/src/mcpEvents/provider.js +296 -0
- package/package.json +9 -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
|
@@ -2,169 +2,19 @@ import { SemanticConditionEngine } from '../semantic/conditionEngine.js';
|
|
|
2
2
|
import { sha256Hex } from '../intelligenceProtocol/canonical.js';
|
|
3
3
|
import { COMPOSITE_TRIGGER_PROTOCOL_VERSION, COMPOSITE_TRIGGER_SCHEMA_VERSION, CompositeTriggerDefinitionSchema, CorrelatableEventSchema, TriggerMatchRecordSchema, } from '../intelligenceProtocol/triggerSchemas.js';
|
|
4
4
|
import { evaluateTemporalConditions, } from './temporal.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if (current === null ||
|
|
9
|
-
typeof current !== 'object' ||
|
|
10
|
-
Array.isArray(current))
|
|
11
|
-
return undefined;
|
|
12
|
-
current = current[part];
|
|
13
|
-
}
|
|
14
|
-
return current;
|
|
15
|
-
}
|
|
16
|
-
function scalarKey(value) {
|
|
17
|
-
if (typeof value === 'string' ||
|
|
18
|
-
typeof value === 'number' ||
|
|
19
|
-
typeof value === 'boolean')
|
|
20
|
-
return String(value);
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
function predicateMatches(event, predicate) {
|
|
24
|
-
const value = getByPath(event.data, predicate.path);
|
|
25
|
-
switch (predicate.op) {
|
|
26
|
-
case 'eq':
|
|
27
|
-
return value === predicate.value;
|
|
28
|
-
case 'neq':
|
|
29
|
-
return value !== predicate.value;
|
|
30
|
-
case 'contains':
|
|
31
|
-
if (typeof value === 'string')
|
|
32
|
-
return value.includes(String(predicate.value));
|
|
33
|
-
if (Array.isArray(value))
|
|
34
|
-
return value.includes(predicate.value);
|
|
35
|
-
return false;
|
|
36
|
-
case 'in':
|
|
37
|
-
return predicate.value.includes(value);
|
|
38
|
-
case 'exists':
|
|
39
|
-
return predicate.value ? value !== undefined : value === undefined;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
function clauseMatches(clause, event) {
|
|
43
|
-
const contractVersion = getByPath(event.data, '_derived.contractVersion');
|
|
44
|
-
return (clause.event === event.name &&
|
|
45
|
-
(!clause.serverId || clause.serverId === event.serverId) &&
|
|
46
|
-
(!clause.contractVersion ||
|
|
47
|
-
String(contractVersion ?? '') === String(clause.contractVersion)) &&
|
|
48
|
-
clause.where.every((predicate) => predicateMatches(event, predicate)));
|
|
49
|
-
}
|
|
50
|
-
function asSourceEvent(clauseId, event) {
|
|
51
|
-
return {
|
|
52
|
-
clauseId,
|
|
53
|
-
traceId: event.traceId,
|
|
54
|
-
sourceEventId: event.sourceEventId,
|
|
55
|
-
eventName: event.name,
|
|
56
|
-
occurredAt: event.occurredAt,
|
|
57
|
-
...(event.provider ? { provider: event.provider } : {}),
|
|
58
|
-
...(event.serverId ? { serverId: event.serverId } : {}),
|
|
59
|
-
...(event.payloadHash ? { payloadHash: event.payloadHash } : {}),
|
|
60
|
-
data: event.data,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
function clauseEvents(record, ref) {
|
|
64
|
-
return record.sourceEvents
|
|
65
|
-
.filter((event) => event.clauseId === ref)
|
|
66
|
-
.sort((a, b) => Date.parse(a.occurredAt) - Date.parse(b.occurredAt));
|
|
67
|
-
}
|
|
68
|
-
function expressionSatisfied(definition, record) {
|
|
69
|
-
const expression = definition.expression;
|
|
70
|
-
if (expression.kind === 'allOf') {
|
|
71
|
-
return expression.refs.every((ref) => clauseEvents(record, ref).length > 0);
|
|
72
|
-
}
|
|
73
|
-
if (expression.kind === 'anyOf') {
|
|
74
|
-
return expression.refs.some((ref) => clauseEvents(record, ref).length > 0);
|
|
75
|
-
}
|
|
76
|
-
if (expression.kind === 'count') {
|
|
77
|
-
return clauseEvents(record, expression.ref).length >= expression.atLeast;
|
|
78
|
-
}
|
|
79
|
-
let previous = Number.NEGATIVE_INFINITY;
|
|
80
|
-
for (const ref of expression.refs) {
|
|
81
|
-
const candidate = clauseEvents(record, ref)
|
|
82
|
-
.find((event) => Date.parse(event.occurredAt) > previous);
|
|
83
|
-
if (!candidate)
|
|
84
|
-
return false;
|
|
85
|
-
previous = Date.parse(candidate.occurredAt);
|
|
86
|
-
}
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
function semanticSource(record) {
|
|
90
|
-
const source = {};
|
|
91
|
-
for (const event of record.sourceEvents) {
|
|
92
|
-
source[event.clauseId] = event.data;
|
|
93
|
-
}
|
|
94
|
-
return source;
|
|
95
|
-
}
|
|
96
|
-
function deterministicKeyForClause(definition, clauseId, event) {
|
|
97
|
-
const deterministic = definition.correlation?.deterministic;
|
|
98
|
-
if (!deterministic)
|
|
99
|
-
return null;
|
|
100
|
-
const field = deterministic.fields.find((item) => item.ref === clauseId);
|
|
101
|
-
if (!field)
|
|
102
|
-
return null;
|
|
103
|
-
return scalarKey(getByPath(event.data, field.path));
|
|
104
|
-
}
|
|
105
|
-
function sameEventIdentity(source, event) {
|
|
106
|
-
return (source.sourceEventId === event.sourceEventId &&
|
|
107
|
-
(source.serverId ?? null) === (event.serverId ?? null));
|
|
108
|
-
}
|
|
109
|
-
function matchesCorrelationKey(record, key) {
|
|
110
|
-
if (record.correlationKey === null || key === null)
|
|
111
|
-
return true;
|
|
112
|
-
return record.correlationKey === key;
|
|
113
|
-
}
|
|
114
|
-
const DERIVED_SERVER_ID = 'event-intelligence:derived';
|
|
115
|
-
function eventNode(eventName, serverId) {
|
|
116
|
-
return `${serverId || DERIVED_SERVER_ID}::${eventName}`;
|
|
117
|
-
}
|
|
118
|
-
function assertNoDerivedEventCycle(existing, candidate, getState) {
|
|
119
|
-
const definitions = [
|
|
120
|
-
...existing.filter((definition) => {
|
|
121
|
-
const state = getState?.(definition.triggerId, definition.version);
|
|
122
|
-
return !state || state.status === 'active' || state.status === 'paused';
|
|
123
|
-
}),
|
|
124
|
-
candidate,
|
|
125
|
-
].filter((definition) => definition.derivedEvent);
|
|
126
|
-
const graph = new Map();
|
|
127
|
-
for (const definition of definitions) {
|
|
128
|
-
const output = eventNode(definition.derivedEvent.name, DERIVED_SERVER_ID);
|
|
129
|
-
for (const clause of definition.clauses) {
|
|
130
|
-
const input = eventNode(clause.event, clause.serverId);
|
|
131
|
-
const edges = graph.get(input) ?? new Set();
|
|
132
|
-
edges.add(output);
|
|
133
|
-
graph.set(input, edges);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
const visiting = new Set();
|
|
137
|
-
const visited = new Set();
|
|
138
|
-
const dfs = (node) => {
|
|
139
|
-
if (visiting.has(node))
|
|
140
|
-
return true;
|
|
141
|
-
if (visited.has(node))
|
|
142
|
-
return false;
|
|
143
|
-
visiting.add(node);
|
|
144
|
-
for (const next of graph.get(node) ?? []) {
|
|
145
|
-
if (dfs(next))
|
|
146
|
-
return true;
|
|
147
|
-
}
|
|
148
|
-
visiting.delete(node);
|
|
149
|
-
visited.add(node);
|
|
150
|
-
return false;
|
|
151
|
-
};
|
|
152
|
-
for (const node of graph.keys()) {
|
|
153
|
-
if (dfs(node)) {
|
|
154
|
-
const error = new Error(`Derived event graph contains a cycle involving ${node}`);
|
|
155
|
-
error.code = 'TRIGGER_DERIVED_EVENT_CYCLE';
|
|
156
|
-
throw error;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
5
|
+
import { asSourceEvent, clauseMatches, deterministicKeyForClause, expressionSatisfied, matchesCorrelationKey, sameEventIdentity, semanticSource, } from './matching.js';
|
|
6
|
+
import { assertNoDerivedEventCycle } from './derivedGraph.js';
|
|
7
|
+
import { CompositeTriggerRuntimeSupport, } from './runtimeSupport.js';
|
|
160
8
|
export class CompositeTriggerEngine {
|
|
161
9
|
store;
|
|
162
10
|
evaluator;
|
|
163
11
|
now;
|
|
12
|
+
support;
|
|
164
13
|
constructor(store, evaluator = null, now = () => new Date()) {
|
|
165
14
|
this.store = store;
|
|
166
15
|
this.evaluator = evaluator;
|
|
167
16
|
this.now = now;
|
|
17
|
+
this.support = new CompositeTriggerRuntimeSupport(store, now);
|
|
168
18
|
}
|
|
169
19
|
async register(definitionInput) {
|
|
170
20
|
const definition = CompositeTriggerDefinitionSchema.parse(definitionInput);
|
|
@@ -180,7 +30,7 @@ export class CompositeTriggerEngine {
|
|
|
180
30
|
const results = [];
|
|
181
31
|
for (const definition of this.store.listTriggers()) {
|
|
182
32
|
const triggerState = this.store.getTriggerState?.(definition.triggerId, definition.version) ?? null;
|
|
183
|
-
if (!(await this.lifecycleAllows(definition, triggerState, this.now()))) {
|
|
33
|
+
if (!(await this.support.lifecycleAllows(definition, triggerState, this.now()))) {
|
|
184
34
|
continue;
|
|
185
35
|
}
|
|
186
36
|
const matchingClauses = definition.clauses.filter((clause) => clauseMatches(clause, event));
|
|
@@ -221,9 +71,9 @@ export class CompositeTriggerEngine {
|
|
|
221
71
|
updatedAt: this.now().toISOString(),
|
|
222
72
|
});
|
|
223
73
|
await this.store.appendTriggerMatch(fired);
|
|
224
|
-
await this.cancelDeadlinesForMatch(matchId);
|
|
225
|
-
await this.
|
|
226
|
-
await this.advanceLifecycleAfterEffect(fired, 'wake', wakeId);
|
|
74
|
+
await this.support.cancelDeadlinesForMatch(matchId);
|
|
75
|
+
await this.support.auditMatch(fired, 'trigger.fired', { wakeId });
|
|
76
|
+
await this.support.advanceLifecycleAfterEffect(fired, 'wake', wakeId);
|
|
227
77
|
return fired;
|
|
228
78
|
}
|
|
229
79
|
async markDerivedEmitted(matchId, derivedEventId) {
|
|
@@ -246,11 +96,11 @@ export class CompositeTriggerEngine {
|
|
|
246
96
|
],
|
|
247
97
|
});
|
|
248
98
|
await this.store.appendTriggerMatch(emitted);
|
|
249
|
-
await this.cancelDeadlinesForMatch(matchId);
|
|
250
|
-
await this.
|
|
99
|
+
await this.support.cancelDeadlinesForMatch(matchId);
|
|
100
|
+
await this.support.auditMatch(emitted, 'trigger.emitted', {
|
|
251
101
|
derivedEventId,
|
|
252
102
|
});
|
|
253
|
-
await this.advanceLifecycleAfterEffect(emitted, 'derived_event', derivedEventId);
|
|
103
|
+
await this.support.advanceLifecycleAfterEffect(emitted, 'derived_event', derivedEventId);
|
|
254
104
|
return emitted;
|
|
255
105
|
}
|
|
256
106
|
async ingestTimerEvent(event) {
|
|
@@ -265,7 +115,7 @@ export class CompositeTriggerEngine {
|
|
|
265
115
|
if (!definition)
|
|
266
116
|
return [];
|
|
267
117
|
const triggerState = this.store.getTriggerState?.(definition.triggerId, definition.version) ?? null;
|
|
268
|
-
if (!(await this.lifecycleAllows(definition, triggerState, this.now()))) {
|
|
118
|
+
if (!(await this.support.lifecycleAllows(definition, triggerState, this.now()))) {
|
|
269
119
|
return [];
|
|
270
120
|
}
|
|
271
121
|
const match = this.latestMatch(deadline.matchId);
|
|
@@ -283,8 +133,8 @@ export class CompositeTriggerEngine {
|
|
|
283
133
|
updatedAt: event.occurredAt,
|
|
284
134
|
});
|
|
285
135
|
await this.store.appendTriggerMatch(expired);
|
|
286
|
-
await this.cancelDeadlinesForMatch(expired.matchId);
|
|
287
|
-
await this.
|
|
136
|
+
await this.support.cancelDeadlinesForMatch(expired.matchId);
|
|
137
|
+
await this.support.auditMatch(expired, 'trigger.expired', {
|
|
288
138
|
reason: 'temporal_deadline_after_event_window',
|
|
289
139
|
});
|
|
290
140
|
return [{
|
|
@@ -323,8 +173,8 @@ export class CompositeTriggerEngine {
|
|
|
323
173
|
updatedAt: nowIso,
|
|
324
174
|
});
|
|
325
175
|
await this.store.appendTriggerMatch(expired);
|
|
326
|
-
await this.cancelDeadlinesForMatch(expired.matchId);
|
|
327
|
-
await this.
|
|
176
|
+
await this.support.cancelDeadlinesForMatch(expired.matchId);
|
|
177
|
+
await this.support.auditMatch(expired, 'trigger.expired', {
|
|
328
178
|
reason: 'window_expired_before_event',
|
|
329
179
|
});
|
|
330
180
|
record = await this.newMatch(definition, key, event);
|
|
@@ -343,7 +193,7 @@ export class CompositeTriggerEngine {
|
|
|
343
193
|
}
|
|
344
194
|
if (!expressionSatisfied(definition, record)) {
|
|
345
195
|
await this.store.appendTriggerMatch(record);
|
|
346
|
-
await this.
|
|
196
|
+
await this.support.auditMatch(record, 'trigger.partial', {
|
|
347
197
|
clauseId: clause.id,
|
|
348
198
|
sourceEventId: event.sourceEventId,
|
|
349
199
|
});
|
|
@@ -366,8 +216,8 @@ export class CompositeTriggerEngine {
|
|
|
366
216
|
updatedAt: nowIso,
|
|
367
217
|
});
|
|
368
218
|
await this.store.appendTriggerMatch(expired);
|
|
369
|
-
await this.cancelDeadlinesForMatch(expired.matchId);
|
|
370
|
-
await this.
|
|
219
|
+
await this.support.cancelDeadlinesForMatch(expired.matchId);
|
|
220
|
+
await this.support.auditMatch(expired, 'trigger.expired', {
|
|
371
221
|
reason: 'temporal_condition_blocked',
|
|
372
222
|
temporal: temporal.conditionStates,
|
|
373
223
|
});
|
|
@@ -384,8 +234,8 @@ export class CompositeTriggerEngine {
|
|
|
384
234
|
status: 'partial',
|
|
385
235
|
updatedAt: nowIso,
|
|
386
236
|
}));
|
|
387
|
-
await this.scheduleDeadlines(definition, record, temporal.pendingDeadlines);
|
|
388
|
-
await this.
|
|
237
|
+
await this.support.scheduleDeadlines(definition, record, temporal.pendingDeadlines);
|
|
238
|
+
await this.support.auditMatch(record, 'trigger.partial', {
|
|
389
239
|
reason: 'temporal_wait',
|
|
390
240
|
temporal: temporal.conditionStates,
|
|
391
241
|
});
|
|
@@ -396,11 +246,11 @@ export class CompositeTriggerEngine {
|
|
|
396
246
|
fired: false,
|
|
397
247
|
};
|
|
398
248
|
}
|
|
399
|
-
await this.cancelDeadlinesForMatch(record.matchId);
|
|
249
|
+
await this.support.cancelDeadlinesForMatch(record.matchId);
|
|
400
250
|
if (definition.correlation?.semantic) {
|
|
401
251
|
if (!this.evaluator) {
|
|
402
252
|
await this.store.appendTriggerMatch(record);
|
|
403
|
-
await this.
|
|
253
|
+
await this.support.auditMatch(record, 'trigger.partial', {
|
|
404
254
|
reason: 'semantic_evaluator_unavailable',
|
|
405
255
|
});
|
|
406
256
|
return {
|
|
@@ -434,7 +284,7 @@ export class CompositeTriggerEngine {
|
|
|
434
284
|
updatedAt: nowIso,
|
|
435
285
|
});
|
|
436
286
|
await this.store.appendTriggerMatch(record);
|
|
437
|
-
await this.
|
|
287
|
+
await this.support.auditMatch(record, 'trigger.correlation', {
|
|
438
288
|
outcome: decision.outcome,
|
|
439
289
|
probability: decision.probability,
|
|
440
290
|
evaluator: decision.evaluator,
|
|
@@ -454,7 +304,7 @@ export class CompositeTriggerEngine {
|
|
|
454
304
|
updatedAt: nowIso,
|
|
455
305
|
});
|
|
456
306
|
await this.store.appendTriggerMatch(matched);
|
|
457
|
-
await this.
|
|
307
|
+
await this.support.auditMatch(matched, 'trigger.matched', {
|
|
458
308
|
sourceEventIds: matched.sourceEvents.map((item) => item.sourceEventId),
|
|
459
309
|
temporal: temporal.conditionStates,
|
|
460
310
|
});
|
|
@@ -465,156 +315,6 @@ export class CompositeTriggerEngine {
|
|
|
465
315
|
fired: false,
|
|
466
316
|
};
|
|
467
317
|
}
|
|
468
|
-
async scheduleDeadlines(definition, record, deadlines) {
|
|
469
|
-
if (!this.store.putTemporalDeadline)
|
|
470
|
-
return;
|
|
471
|
-
for (const deadline of deadlines) {
|
|
472
|
-
const deadlineId = `td_${(await sha256Hex([
|
|
473
|
-
definition.triggerId,
|
|
474
|
-
definition.version,
|
|
475
|
-
record.matchId,
|
|
476
|
-
deadline.conditionId,
|
|
477
|
-
deadline.dueAt,
|
|
478
|
-
].join(':'))).slice(0, 24)}`;
|
|
479
|
-
const existing = this.store.getTemporalDeadline?.(deadlineId);
|
|
480
|
-
if (existing?.status === 'pending' || existing?.status === 'fired') {
|
|
481
|
-
continue;
|
|
482
|
-
}
|
|
483
|
-
await this.store.putTemporalDeadline({
|
|
484
|
-
deadlineId,
|
|
485
|
-
triggerId: definition.triggerId,
|
|
486
|
-
triggerVersion: definition.version,
|
|
487
|
-
matchId: record.matchId,
|
|
488
|
-
conditionId: deadline.conditionId,
|
|
489
|
-
dueAt: deadline.dueAt,
|
|
490
|
-
status: 'pending',
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
async cancelDeadlinesForMatch(matchId) {
|
|
495
|
-
if (!this.store.listTemporalDeadlines ||
|
|
496
|
-
!this.store.setTemporalDeadlineStatus)
|
|
497
|
-
return;
|
|
498
|
-
const pending = this.store.listTemporalDeadlines({
|
|
499
|
-
matchId,
|
|
500
|
-
status: 'pending',
|
|
501
|
-
});
|
|
502
|
-
for (const deadline of pending) {
|
|
503
|
-
await this.store.setTemporalDeadlineStatus(deadline.deadlineId, 'cancelled');
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
async advanceLifecycleAfterEffect(match, effectKind, effectId) {
|
|
507
|
-
const definition = this.store.listTriggers().find((candidate) => candidate.triggerId === match.triggerId &&
|
|
508
|
-
candidate.version === match.triggerVersion);
|
|
509
|
-
const triggerState = this.store.getTriggerState?.(match.triggerId, match.triggerVersion) ?? null;
|
|
510
|
-
if (!definition || !this.store.setTriggerState)
|
|
511
|
-
return;
|
|
512
|
-
const previousCount = triggerState?.fireCount ?? 0;
|
|
513
|
-
const fireCount = previousCount + 1;
|
|
514
|
-
const lifecycle = definition.lifecycle ?? {};
|
|
515
|
-
const complete = lifecycle.oneShot === true ||
|
|
516
|
-
lifecycle.completeOnGoal === true ||
|
|
517
|
-
(lifecycle.maxFirings !== undefined &&
|
|
518
|
-
fireCount >= lifecycle.maxFirings);
|
|
519
|
-
const nextStatus = complete ? 'completed' : 'active';
|
|
520
|
-
const effectAt = this.now().toISOString();
|
|
521
|
-
await this.store.setTriggerState(match.triggerId, match.triggerVersion, nextStatus, {
|
|
522
|
-
type: 'system',
|
|
523
|
-
principal_id: 'event-intelligence:lifecycle',
|
|
524
|
-
}, {
|
|
525
|
-
owner: triggerState?.owner ?? null,
|
|
526
|
-
connectionIds: triggerState?.connectionIds ?? [],
|
|
527
|
-
fireCount,
|
|
528
|
-
lastFiredAt: effectAt,
|
|
529
|
-
});
|
|
530
|
-
if (complete) {
|
|
531
|
-
await this.auditTriggerLifecycle(definition, 'trigger.completed', {
|
|
532
|
-
reason: lifecycle.oneShot
|
|
533
|
-
? 'one_shot'
|
|
534
|
-
: lifecycle.completeOnGoal
|
|
535
|
-
? 'complete_on_goal'
|
|
536
|
-
: 'max_firings',
|
|
537
|
-
fireCount,
|
|
538
|
-
effectKind,
|
|
539
|
-
effectId,
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
async lifecycleAllows(definition, state, now) {
|
|
544
|
-
const lifecycle = definition.lifecycle ?? {};
|
|
545
|
-
const effectiveState = state ?? {
|
|
546
|
-
status: 'active',
|
|
547
|
-
fireCount: 0,
|
|
548
|
-
lastFiredAt: null,
|
|
549
|
-
connectionIds: [],
|
|
550
|
-
owner: null,
|
|
551
|
-
};
|
|
552
|
-
if (effectiveState.status !== 'active')
|
|
553
|
-
return false;
|
|
554
|
-
const expiresAt = lifecycle.expiresAt
|
|
555
|
-
? Date.parse(lifecycle.expiresAt)
|
|
556
|
-
: Number.POSITIVE_INFINITY;
|
|
557
|
-
const leaseUntil = lifecycle.leaseUntil
|
|
558
|
-
? Date.parse(lifecycle.leaseUntil)
|
|
559
|
-
: Number.POSITIVE_INFINITY;
|
|
560
|
-
if (now.getTime() > Math.min(expiresAt, leaseUntil)) {
|
|
561
|
-
if (this.store.setTriggerState) {
|
|
562
|
-
await this.store.setTriggerState(definition.triggerId, definition.version, 'expired', {
|
|
563
|
-
type: 'system',
|
|
564
|
-
principal_id: 'event-intelligence:lifecycle',
|
|
565
|
-
}, {
|
|
566
|
-
owner: effectiveState.owner ?? null,
|
|
567
|
-
connectionIds: effectiveState.connectionIds ?? [],
|
|
568
|
-
fireCount: effectiveState.fireCount ?? 0,
|
|
569
|
-
lastFiredAt: effectiveState.lastFiredAt ?? null,
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
await this.auditTriggerLifecycle(definition, 'trigger.lifecycle_expired', {
|
|
573
|
-
expiresAt: lifecycle.expiresAt ?? null,
|
|
574
|
-
leaseUntil: lifecycle.leaseUntil ?? null,
|
|
575
|
-
});
|
|
576
|
-
return false;
|
|
577
|
-
}
|
|
578
|
-
const fireCount = effectiveState.fireCount ?? 0;
|
|
579
|
-
if ((lifecycle.oneShot && fireCount >= 1) ||
|
|
580
|
-
(lifecycle.maxFirings !== undefined &&
|
|
581
|
-
fireCount >= lifecycle.maxFirings)) {
|
|
582
|
-
if (this.store.setTriggerState) {
|
|
583
|
-
await this.store.setTriggerState(definition.triggerId, definition.version, 'completed', {
|
|
584
|
-
type: 'system',
|
|
585
|
-
principal_id: 'event-intelligence:lifecycle',
|
|
586
|
-
}, {
|
|
587
|
-
owner: effectiveState.owner ?? null,
|
|
588
|
-
connectionIds: effectiveState.connectionIds ?? [],
|
|
589
|
-
fireCount,
|
|
590
|
-
lastFiredAt: effectiveState.lastFiredAt ?? null,
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
return false;
|
|
594
|
-
}
|
|
595
|
-
if (lifecycle.cooldownMs > 0 &&
|
|
596
|
-
effectiveState.lastFiredAt &&
|
|
597
|
-
now.getTime() - Date.parse(effectiveState.lastFiredAt) <
|
|
598
|
-
lifecycle.cooldownMs) {
|
|
599
|
-
return false;
|
|
600
|
-
}
|
|
601
|
-
return true;
|
|
602
|
-
}
|
|
603
|
-
async auditTriggerLifecycle(definition, kind, details) {
|
|
604
|
-
const timestamp = this.now().toISOString();
|
|
605
|
-
await this.store.appendAudit({
|
|
606
|
-
auditId: `audit_${(await sha256Hex(`${definition.triggerId}:${definition.version}:${kind}:${timestamp}`)).slice(0, 24)}`,
|
|
607
|
-
traceId: `trigger:${definition.triggerId}`,
|
|
608
|
-
timestamp,
|
|
609
|
-
kind,
|
|
610
|
-
entityType: 'trigger',
|
|
611
|
-
entityId: definition.triggerId,
|
|
612
|
-
details: {
|
|
613
|
-
triggerVersion: definition.version,
|
|
614
|
-
...details,
|
|
615
|
-
},
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
318
|
async newMatch(definition, key, event) {
|
|
619
319
|
const openedAt = event.occurredAt;
|
|
620
320
|
const expiresAt = new Date(Date.parse(openedAt) + definition.withinMs).toISOString();
|
|
@@ -647,21 +347,4 @@ export class CompositeTriggerEngine {
|
|
|
647
347
|
.filter((record) => record.matchId === matchId)
|
|
648
348
|
.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0] ?? null;
|
|
649
349
|
}
|
|
650
|
-
async audit(record, kind, details = {}) {
|
|
651
|
-
await this.store.appendAudit({
|
|
652
|
-
auditId: `audit_${(await sha256Hex(`${record.matchId}:${kind}:${record.updatedAt}:${this.store.listTriggerMatches().length}`)).slice(0, 24)}`,
|
|
653
|
-
traceId: record.sourceEvents[0]?.traceId ?? record.matchId,
|
|
654
|
-
timestamp: this.now().toISOString(),
|
|
655
|
-
kind,
|
|
656
|
-
entityType: 'trigger_match',
|
|
657
|
-
entityId: record.matchId,
|
|
658
|
-
details: {
|
|
659
|
-
triggerId: record.triggerId,
|
|
660
|
-
triggerVersion: record.triggerVersion,
|
|
661
|
-
status: record.status,
|
|
662
|
-
correlationKey: record.correlationKey,
|
|
663
|
-
...details,
|
|
664
|
-
},
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
350
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CompositeTriggerDefinition, CorrelatableEvent, TriggerClause, TriggerMatchRecord, TriggerSourceEvent } from '../intelligenceProtocol/triggerSchemas.js';
|
|
2
|
+
export declare function clauseMatches(clause: TriggerClause, event: CorrelatableEvent): boolean;
|
|
3
|
+
export declare function asSourceEvent(clauseId: string, event: CorrelatableEvent): TriggerSourceEvent;
|
|
4
|
+
export declare function expressionSatisfied(definition: CompositeTriggerDefinition, record: TriggerMatchRecord): boolean;
|
|
5
|
+
export declare function semanticSource(record: TriggerMatchRecord): Record<string, unknown>;
|
|
6
|
+
export declare function deterministicKeyForClause(definition: CompositeTriggerDefinition, clauseId: string, event: CorrelatableEvent): string | null;
|
|
7
|
+
export declare function sameEventIdentity(source: Pick<TriggerSourceEvent, 'sourceEventId' | 'serverId'>, event: Pick<CorrelatableEvent, 'sourceEventId' | 'serverId'>): boolean;
|
|
8
|
+
export declare function matchesCorrelationKey(record: TriggerMatchRecord, key: string | null): boolean;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
function getByPath(source, path) {
|
|
2
|
+
let current = source;
|
|
3
|
+
for (const part of path.split('.')) {
|
|
4
|
+
if (current === null ||
|
|
5
|
+
typeof current !== 'object' ||
|
|
6
|
+
Array.isArray(current))
|
|
7
|
+
return undefined;
|
|
8
|
+
current = current[part];
|
|
9
|
+
}
|
|
10
|
+
return current;
|
|
11
|
+
}
|
|
12
|
+
function scalarKey(value) {
|
|
13
|
+
if (typeof value === 'string' ||
|
|
14
|
+
typeof value === 'number' ||
|
|
15
|
+
typeof value === 'boolean')
|
|
16
|
+
return String(value);
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
function predicateMatches(event, predicate) {
|
|
20
|
+
const value = getByPath(event.data, predicate.path);
|
|
21
|
+
switch (predicate.op) {
|
|
22
|
+
case 'eq':
|
|
23
|
+
return value === predicate.value;
|
|
24
|
+
case 'neq':
|
|
25
|
+
return value !== predicate.value;
|
|
26
|
+
case 'contains':
|
|
27
|
+
if (typeof value === 'string')
|
|
28
|
+
return value.includes(String(predicate.value));
|
|
29
|
+
if (Array.isArray(value))
|
|
30
|
+
return value.includes(predicate.value);
|
|
31
|
+
return false;
|
|
32
|
+
case 'in':
|
|
33
|
+
return predicate.value.includes(value);
|
|
34
|
+
case 'exists':
|
|
35
|
+
return predicate.value ? value !== undefined : value === undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function clauseMatches(clause, event) {
|
|
39
|
+
const contractVersion = getByPath(event.data, '_derived.contractVersion');
|
|
40
|
+
return (clause.event === event.name &&
|
|
41
|
+
(!clause.serverId || clause.serverId === event.serverId) &&
|
|
42
|
+
(!clause.contractVersion ||
|
|
43
|
+
String(contractVersion ?? '') === String(clause.contractVersion)) &&
|
|
44
|
+
clause.where.every((predicate) => predicateMatches(event, predicate)));
|
|
45
|
+
}
|
|
46
|
+
export function asSourceEvent(clauseId, event) {
|
|
47
|
+
return {
|
|
48
|
+
clauseId,
|
|
49
|
+
traceId: event.traceId,
|
|
50
|
+
sourceEventId: event.sourceEventId,
|
|
51
|
+
eventName: event.name,
|
|
52
|
+
occurredAt: event.occurredAt,
|
|
53
|
+
...(event.provider ? { provider: event.provider } : {}),
|
|
54
|
+
...(event.serverId ? { serverId: event.serverId } : {}),
|
|
55
|
+
...(event.payloadHash ? { payloadHash: event.payloadHash } : {}),
|
|
56
|
+
data: event.data,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function clauseEvents(record, ref) {
|
|
60
|
+
return record.sourceEvents
|
|
61
|
+
.filter((event) => event.clauseId === ref)
|
|
62
|
+
.sort((a, b) => Date.parse(a.occurredAt) - Date.parse(b.occurredAt));
|
|
63
|
+
}
|
|
64
|
+
export function expressionSatisfied(definition, record) {
|
|
65
|
+
const expression = definition.expression;
|
|
66
|
+
if (expression.kind === 'allOf') {
|
|
67
|
+
return expression.refs.every((ref) => clauseEvents(record, ref).length > 0);
|
|
68
|
+
}
|
|
69
|
+
if (expression.kind === 'anyOf') {
|
|
70
|
+
return expression.refs.some((ref) => clauseEvents(record, ref).length > 0);
|
|
71
|
+
}
|
|
72
|
+
if (expression.kind === 'count') {
|
|
73
|
+
return clauseEvents(record, expression.ref).length >= expression.atLeast;
|
|
74
|
+
}
|
|
75
|
+
let previous = Number.NEGATIVE_INFINITY;
|
|
76
|
+
for (const ref of expression.refs) {
|
|
77
|
+
const candidate = clauseEvents(record, ref)
|
|
78
|
+
.find((event) => Date.parse(event.occurredAt) > previous);
|
|
79
|
+
if (!candidate)
|
|
80
|
+
return false;
|
|
81
|
+
previous = Date.parse(candidate.occurredAt);
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
export function semanticSource(record) {
|
|
86
|
+
const source = {};
|
|
87
|
+
for (const event of record.sourceEvents) {
|
|
88
|
+
source[event.clauseId] = event.data;
|
|
89
|
+
}
|
|
90
|
+
return source;
|
|
91
|
+
}
|
|
92
|
+
export function deterministicKeyForClause(definition, clauseId, event) {
|
|
93
|
+
const deterministic = definition.correlation?.deterministic;
|
|
94
|
+
if (!deterministic)
|
|
95
|
+
return null;
|
|
96
|
+
const field = deterministic.fields.find((item) => item.ref === clauseId);
|
|
97
|
+
if (!field)
|
|
98
|
+
return null;
|
|
99
|
+
return scalarKey(getByPath(event.data, field.path));
|
|
100
|
+
}
|
|
101
|
+
export function sameEventIdentity(source, event) {
|
|
102
|
+
return (source.sourceEventId === event.sourceEventId &&
|
|
103
|
+
(source.serverId ?? null) === (event.serverId ?? null));
|
|
104
|
+
}
|
|
105
|
+
export function matchesCorrelationKey(record, key) {
|
|
106
|
+
if (record.correlationKey === null || key === null)
|
|
107
|
+
return true;
|
|
108
|
+
return record.correlationKey === key;
|
|
109
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CompositeTriggerDefinition, TriggerMatchRecord } from '../intelligenceProtocol/triggerSchemas.js';
|
|
2
|
+
import type { CompositeTriggerAudit, CompositeTriggerStore } from './store.js';
|
|
3
|
+
type TriggerState = {
|
|
4
|
+
status: 'active' | 'paused' | 'completed' | 'expired' | 'deleted';
|
|
5
|
+
owner?: unknown;
|
|
6
|
+
connectionIds?: string[];
|
|
7
|
+
fireCount?: number;
|
|
8
|
+
lastFiredAt?: string | null;
|
|
9
|
+
};
|
|
10
|
+
export declare class CompositeTriggerRuntimeSupport {
|
|
11
|
+
private readonly store;
|
|
12
|
+
private readonly now;
|
|
13
|
+
constructor(store: CompositeTriggerStore & CompositeTriggerAudit, now?: () => Date);
|
|
14
|
+
scheduleDeadlines(definition: CompositeTriggerDefinition, record: TriggerMatchRecord, deadlines: Array<{
|
|
15
|
+
conditionId: string;
|
|
16
|
+
dueAt: string;
|
|
17
|
+
}>): Promise<void>;
|
|
18
|
+
cancelDeadlinesForMatch(matchId: string): Promise<void>;
|
|
19
|
+
advanceLifecycleAfterEffect(match: TriggerMatchRecord, effectKind: 'wake' | 'derived_event', effectId: string): Promise<void>;
|
|
20
|
+
lifecycleAllows(definition: CompositeTriggerDefinition, state: TriggerState | null, now: Date): Promise<boolean>;
|
|
21
|
+
auditTriggerLifecycle(definition: CompositeTriggerDefinition, kind: string, details: Record<string, unknown>): Promise<void>;
|
|
22
|
+
auditMatch(record: TriggerMatchRecord, kind: 'trigger.partial' | 'trigger.matched' | 'trigger.fired' | 'trigger.emitted' | 'trigger.expired' | 'trigger.correlation', details?: Record<string, unknown>): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export {};
|