mcp-event-intelligence 0.3.1 → 0.5.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 +81 -0
- package/README.md +123 -23
- package/SECURITY.md +4 -4
- package/bin/mcp-event-intelligence.mjs +3 -3
- package/dist/src/composite/engine.d.ts +6 -0
- package/dist/src/composite/engine.js +599 -10
- package/dist/src/composite/matching.d.ts +1 -1
- package/dist/src/composite/matching.js +66 -1
- package/dist/src/intelligenceProtocol/activationSchemas.d.ts +2 -0
- package/dist/src/intelligenceProtocol/activationSchemas.js +1 -0
- package/dist/src/intelligenceProtocol/index.d.ts +9 -2
- package/dist/src/intelligenceProtocol/index.js +5 -1
- package/dist/src/intelligenceProtocol/operatorRegistry.d.ts +567 -0
- package/dist/src/intelligenceProtocol/operatorRegistry.js +477 -0
- package/dist/src/intelligenceProtocol/patternV2Schemas.d.ts +188 -0
- package/dist/src/intelligenceProtocol/patternV2Schemas.js +801 -0
- package/dist/src/intelligenceProtocol/schemas.d.ts +6 -0
- package/dist/src/intelligenceProtocol/schemas.js +3 -0
- package/dist/src/intelligenceProtocol/triggerPlanSchemas.d.ts +179 -4
- package/dist/src/intelligenceProtocol/triggerPlanSchemas.js +38 -4
- package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +262 -0
- package/dist/src/intelligenceProtocol/triggerSchemas.js +164 -2
- package/dist/src/mcpEvents/consumer.d.ts +2 -0
- package/dist/src/mcpEvents/consumer.js +2 -0
- package/dist/src/mcpEvents/erpnextProvider.d.ts +0 -2
- package/dist/src/mcpEvents/erpnextProvider.js +0 -3
- package/dist/src/mcpEvents/provider.d.ts +24 -10
- package/dist/src/mcpEvents/provider.js +124 -55
- package/dist/src/mcpEvents/server.d.ts +3 -1
- package/dist/src/mcpEvents/server.js +20 -19
- package/dist/src/patternV2/evaluator.d.ts +43 -0
- package/dist/src/patternV2/evaluator.js +927 -0
- package/dist/src/patternV2/legacyCompiler.d.ts +3 -0
- package/dist/src/patternV2/legacyCompiler.js +184 -0
- package/dist/src/protocol/types.d.ts +1 -0
- package/package.json +5 -2
- package/scripts/host-integration.d.mts +47 -0
- package/scripts/host-integration.mjs +9 -0
- package/scripts/lib/activation-hydrator.mjs +1 -0
- package/scripts/lib/composite-event-consumer.mjs +4 -0
- package/scripts/lib/derived-event-coordinator.mjs +75 -1
- package/scripts/lib/json-schema.mjs +134 -0
- package/scripts/lib/local-event-intelligence-runtime.mjs +1 -0
- package/scripts/lib/mcp-events-client.mjs +798 -253
- package/scripts/lib/mcp-events-compatibility.mjs +54 -0
- package/scripts/lib/persistent-event-store.mjs +68 -9
- package/scripts/lib/trigger-control-plane.mjs +180 -0
- package/scripts/lib/trigger-inspector.mjs +15 -0
- package/scripts/lib/trigger-planner.mjs +213 -12
- package/scripts/mcp-stdio-server.mjs +38 -52
- package/server.json +4 -4
|
@@ -1,10 +1,75 @@
|
|
|
1
1
|
import { SemanticConditionEngine } from '../semantic/conditionEngine.js';
|
|
2
|
+
import { evaluatePatternV2, patternV2CandidateSignature, } from '../patternV2/evaluator.js';
|
|
2
3
|
import { sha256Hex } from '../intelligenceProtocol/canonical.js';
|
|
3
4
|
import { COMPOSITE_TRIGGER_PROTOCOL_VERSION, COMPOSITE_TRIGGER_SCHEMA_VERSION, CompositeTriggerDefinitionSchema, CorrelatableEventSchema, TriggerMatchRecordSchema, } from '../intelligenceProtocol/triggerSchemas.js';
|
|
4
5
|
import { evaluateTemporalConditions, } from './temporal.js';
|
|
5
6
|
import { asSourceEvent, clauseMatches, deterministicKeyForClause, expressionSatisfied, matchesCorrelationKey, sameEventIdentity, semanticSource, } from './matching.js';
|
|
6
7
|
import { assertNoDerivedEventCycle } from './derivedGraph.js';
|
|
7
8
|
import { CompositeTriggerRuntimeSupport, } from './runtimeSupport.js';
|
|
9
|
+
function clauseAccumulatesOccurrences(definition, clauseId) {
|
|
10
|
+
if (definition.correlation?.semantic)
|
|
11
|
+
return true;
|
|
12
|
+
if (definition.expression.kind === 'count' &&
|
|
13
|
+
definition.expression.ref === clauseId) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
return definition.temporal.some((condition) => (condition.kind === 'threshold' ||
|
|
17
|
+
condition.kind === 'rate' ||
|
|
18
|
+
condition.kind === 'distinct' ||
|
|
19
|
+
condition.kind === 'debounce') &&
|
|
20
|
+
condition.ref === clauseId);
|
|
21
|
+
}
|
|
22
|
+
function getByPath(source, path) {
|
|
23
|
+
let current = source;
|
|
24
|
+
for (const part of path.split('.')) {
|
|
25
|
+
if (current === null ||
|
|
26
|
+
typeof current !== 'object' ||
|
|
27
|
+
Array.isArray(current))
|
|
28
|
+
return undefined;
|
|
29
|
+
current = current[part];
|
|
30
|
+
}
|
|
31
|
+
return current;
|
|
32
|
+
}
|
|
33
|
+
function sourceEventOrder(left, right) {
|
|
34
|
+
return (Date.parse(left.occurredAt) - Date.parse(right.occurredAt) ||
|
|
35
|
+
left.sourceEventId.localeCompare(right.sourceEventId) ||
|
|
36
|
+
left.clauseId.localeCompare(right.clauseId));
|
|
37
|
+
}
|
|
38
|
+
function stablePatternValue(value) {
|
|
39
|
+
if (Array.isArray(value))
|
|
40
|
+
return value.map(stablePatternValue);
|
|
41
|
+
if (value && typeof value === 'object') {
|
|
42
|
+
return Object.fromEntries(Object.keys(value)
|
|
43
|
+
.sort()
|
|
44
|
+
.map((key) => [
|
|
45
|
+
key,
|
|
46
|
+
stablePatternValue(value[key]),
|
|
47
|
+
]));
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
function patternPhysicalIdentity(event) {
|
|
52
|
+
return [
|
|
53
|
+
event.serverId ?? '-',
|
|
54
|
+
event.sourceEventId,
|
|
55
|
+
JSON.stringify(stablePatternValue(event.subscriptionArguments ?? {})),
|
|
56
|
+
].join(':');
|
|
57
|
+
}
|
|
58
|
+
function prospectiveEventWindow(record, event, withinMs) {
|
|
59
|
+
const eventTimes = [
|
|
60
|
+
...record.sourceEvents.map((source) => Date.parse(source.occurredAt)),
|
|
61
|
+
Date.parse(event.occurredAt),
|
|
62
|
+
];
|
|
63
|
+
const openedMs = Math.min(...eventTimes);
|
|
64
|
+
const latestMs = Math.max(...eventTimes);
|
|
65
|
+
const spanMs = latestMs - openedMs;
|
|
66
|
+
return {
|
|
67
|
+
openedAt: new Date(openedMs).toISOString(),
|
|
68
|
+
expiresAt: new Date(openedMs + withinMs).toISOString(),
|
|
69
|
+
spanMs,
|
|
70
|
+
fits: spanMs <= withinMs,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
8
73
|
export class CompositeTriggerEngine {
|
|
9
74
|
store;
|
|
10
75
|
evaluator;
|
|
@@ -36,6 +101,38 @@ export class CompositeTriggerEngine {
|
|
|
36
101
|
const matchingClauses = definition.clauses.filter((clause) => clauseMatches(clause, event));
|
|
37
102
|
if (matchingClauses.length === 0)
|
|
38
103
|
continue;
|
|
104
|
+
if (definition.patternV2) {
|
|
105
|
+
const replayed = this.store
|
|
106
|
+
.listTriggerMatches(definition.triggerId)
|
|
107
|
+
.filter((record) => record.triggerVersion === definition.version &&
|
|
108
|
+
record.sourceEvents.some((source) => sameEventIdentity(source, event)))
|
|
109
|
+
.sort((a, b) => {
|
|
110
|
+
const rank = (status) => status === 'fired'
|
|
111
|
+
? 4
|
|
112
|
+
: status === 'emitted'
|
|
113
|
+
? 3
|
|
114
|
+
: status === 'matched'
|
|
115
|
+
? 2
|
|
116
|
+
: status === 'partial'
|
|
117
|
+
? 1
|
|
118
|
+
: 0;
|
|
119
|
+
return (rank(b.status) - rank(a.status) ||
|
|
120
|
+
Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
|
|
121
|
+
})[0];
|
|
122
|
+
if (replayed) {
|
|
123
|
+
results.push({
|
|
124
|
+
triggerId: definition.triggerId,
|
|
125
|
+
match: replayed,
|
|
126
|
+
matched: replayed.status === 'matched' ||
|
|
127
|
+
replayed.status === 'emitted' ||
|
|
128
|
+
replayed.status === 'fired',
|
|
129
|
+
fired: replayed.status === 'fired',
|
|
130
|
+
});
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
results.push(...await this.applyPatternV2(definition, matchingClauses, event));
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
39
136
|
const replayed = this.store
|
|
40
137
|
.listTriggerMatches(definition.triggerId)
|
|
41
138
|
.find((record) => record.sourceEvents.some((source) => sameEventIdentity(source, event)));
|
|
@@ -126,6 +223,11 @@ export class CompositeTriggerEngine {
|
|
|
126
223
|
match.status === 'expired') {
|
|
127
224
|
return [];
|
|
128
225
|
}
|
|
226
|
+
if (definition.patternV2 &&
|
|
227
|
+
match.patternState?.version === '2' &&
|
|
228
|
+
match.patternState.role === 'buffer') {
|
|
229
|
+
return this.evaluatePatternV2Buffer(definition, match, new Date(event.occurredAt));
|
|
230
|
+
}
|
|
129
231
|
if (Date.parse(event.occurredAt) > Date.parse(match.expiresAt)) {
|
|
130
232
|
const expired = TriggerMatchRecordSchema.parse({
|
|
131
233
|
...match,
|
|
@@ -147,43 +249,506 @@ export class CompositeTriggerEngine {
|
|
|
147
249
|
const result = await this.finalizeEligible(definition, match, new Date(event.occurredAt));
|
|
148
250
|
return [result];
|
|
149
251
|
}
|
|
252
|
+
patternPartitionKey(definition, matchingClauses, event) {
|
|
253
|
+
const dimensions = definition.patternV2?.partitionBy ?? [];
|
|
254
|
+
if (!dimensions.length)
|
|
255
|
+
return null;
|
|
256
|
+
const clauseIds = new Set(matchingClauses.map((clause) => clause.id));
|
|
257
|
+
const components = [];
|
|
258
|
+
for (const dimension of dimensions) {
|
|
259
|
+
const fields = dimension.fields.filter((field) => clauseIds.has(field.ref));
|
|
260
|
+
if (!fields.length) {
|
|
261
|
+
throw new Error(`Pattern partition ${dimension.key} has no field for incoming event ${event.name}`);
|
|
262
|
+
}
|
|
263
|
+
const values = fields.map((field) => getByPath(event.data, field.path));
|
|
264
|
+
const first = values[0];
|
|
265
|
+
if (first === undefined ||
|
|
266
|
+
first === null ||
|
|
267
|
+
!['string', 'number', 'boolean'].includes(typeof first)) {
|
|
268
|
+
throw new Error(`Pattern partition ${dimension.key} resolved to a non-scalar value`);
|
|
269
|
+
}
|
|
270
|
+
if (values.some((value) => typeof value !== typeof first ||
|
|
271
|
+
value !== first)) {
|
|
272
|
+
throw new Error(`Pattern partition ${dimension.key} is ambiguous for incoming event ${event.name}`);
|
|
273
|
+
}
|
|
274
|
+
components.push({
|
|
275
|
+
key: dimension.key,
|
|
276
|
+
type: typeof first,
|
|
277
|
+
value: first,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return JSON.stringify(components);
|
|
281
|
+
}
|
|
282
|
+
async patternBufferId(definition, key) {
|
|
283
|
+
return `tm_${(await sha256Hex([
|
|
284
|
+
definition.triggerId,
|
|
285
|
+
definition.version,
|
|
286
|
+
key ?? '-',
|
|
287
|
+
'pattern-v2-buffer',
|
|
288
|
+
].join(':'))).slice(0, 24)}`;
|
|
289
|
+
}
|
|
290
|
+
patternDecisionRecord(item, evaluatedAt) {
|
|
291
|
+
const decision = item.decision;
|
|
292
|
+
return {
|
|
293
|
+
nodeId: item.nodeId,
|
|
294
|
+
decision: {
|
|
295
|
+
evaluator: decision.evaluator,
|
|
296
|
+
outcome: decision.outcome,
|
|
297
|
+
probability: decision.probability,
|
|
298
|
+
matched: decision.matched,
|
|
299
|
+
shouldEscalate: decision.shouldEscalate,
|
|
300
|
+
inputFields: decision.condition.input,
|
|
301
|
+
...(decision.metadata
|
|
302
|
+
? { providerEvidence: decision.metadata }
|
|
303
|
+
: {}),
|
|
304
|
+
evaluatedAt,
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
async applyPatternV2(definition, matchingClauses, event) {
|
|
309
|
+
const pattern = definition.patternV2;
|
|
310
|
+
if (!pattern)
|
|
311
|
+
return [];
|
|
312
|
+
const now = this.now();
|
|
313
|
+
const nowIso = now.toISOString();
|
|
314
|
+
const key = this.patternPartitionKey(definition, matchingClauses, event);
|
|
315
|
+
const bufferId = await this.patternBufferId(definition, key);
|
|
316
|
+
const existing = this.latestMatch(bufferId);
|
|
317
|
+
let sourceEvents = existing?.sourceEvents
|
|
318
|
+
? [...existing.sourceEvents]
|
|
319
|
+
: [];
|
|
320
|
+
for (const clause of matchingClauses) {
|
|
321
|
+
const duplicate = sourceEvents.some((source) => source.clauseId === clause.id &&
|
|
322
|
+
sameEventIdentity(source, event));
|
|
323
|
+
if (!duplicate) {
|
|
324
|
+
sourceEvents.push(asSourceEvent(clause.id, event));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
sourceEvents.sort(sourceEventOrder);
|
|
328
|
+
const previousMaxObserved = existing?.patternState
|
|
329
|
+
?.maxObservedOccurredAt
|
|
330
|
+
? Date.parse(existing.patternState.maxObservedOccurredAt)
|
|
331
|
+
: Number.NEGATIVE_INFINITY;
|
|
332
|
+
const maxObserved = Math.max(previousMaxObserved, Date.parse(event.occurredAt), ...sourceEvents
|
|
333
|
+
.map((source) => Date.parse(source.occurredAt))
|
|
334
|
+
.filter(Number.isFinite));
|
|
335
|
+
const allowedLatenessMs = definition.eventTime?.allowedLatenessMs ?? 0;
|
|
336
|
+
const watermarkMs = maxObserved - allowedLatenessMs;
|
|
337
|
+
const retentionFloor = watermarkMs - definition.withinMs;
|
|
338
|
+
sourceEvents = sourceEvents.filter((source) => Date.parse(source.occurredAt) >= retentionFloor);
|
|
339
|
+
const bufferedPhysicalEvents = new Set(sourceEvents.map(patternPhysicalIdentity)).size;
|
|
340
|
+
if (bufferedPhysicalEvents > pattern.execution.maxBufferedEvents) {
|
|
341
|
+
await this.store.appendAudit({
|
|
342
|
+
auditId: `audit_${(await sha256Hex(`${definition.triggerId}:${definition.version}:pattern_buffer_overflow:${key ?? '-'}:${nowIso}`)).slice(0, 24)}`,
|
|
343
|
+
traceId: event.traceId,
|
|
344
|
+
timestamp: nowIso,
|
|
345
|
+
kind: 'event.buffer_overflow',
|
|
346
|
+
entityType: 'trigger',
|
|
347
|
+
entityId: definition.triggerId,
|
|
348
|
+
details: {
|
|
349
|
+
triggerVersion: definition.version,
|
|
350
|
+
patternVersion: '2',
|
|
351
|
+
correlationKey: key,
|
|
352
|
+
bufferedEvents: bufferedPhysicalEvents,
|
|
353
|
+
maxBufferedEvents: pattern.execution.maxBufferedEvents,
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
const error = new Error(`Pattern v2 buffer exceeded maxBufferedEvents=${pattern.execution.maxBufferedEvents}`);
|
|
357
|
+
error.code =
|
|
358
|
+
'PATTERN_V2_BUFFER_LIMIT_EXCEEDED';
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
if (Date.parse(event.occurredAt) < retentionFloor &&
|
|
362
|
+
!existing?.sourceEvents.some((source) => sameEventIdentity(source, event))) {
|
|
363
|
+
await this.store.appendAudit({
|
|
364
|
+
auditId: `audit_${(await sha256Hex(`${definition.triggerId}:${definition.version}:pattern_late_event_dropped:${event.sourceEventId}:${nowIso}`)).slice(0, 24)}`,
|
|
365
|
+
traceId: event.traceId,
|
|
366
|
+
timestamp: nowIso,
|
|
367
|
+
kind: 'event.late_dropped',
|
|
368
|
+
entityType: 'trigger',
|
|
369
|
+
entityId: definition.triggerId,
|
|
370
|
+
details: {
|
|
371
|
+
triggerVersion: definition.version,
|
|
372
|
+
patternVersion: '2',
|
|
373
|
+
sourceEventId: event.sourceEventId,
|
|
374
|
+
occurredAt: event.occurredAt,
|
|
375
|
+
watermarkAt: new Date(watermarkMs).toISOString(),
|
|
376
|
+
retentionFloor: new Date(retentionFloor).toISOString(),
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
return [{
|
|
380
|
+
triggerId: definition.triggerId,
|
|
381
|
+
match: existing ?? null,
|
|
382
|
+
matched: false,
|
|
383
|
+
fired: false,
|
|
384
|
+
}];
|
|
385
|
+
}
|
|
386
|
+
const openedAt = sourceEvents[0]?.occurredAt ?? event.occurredAt;
|
|
387
|
+
const buffer = TriggerMatchRecordSchema.parse({
|
|
388
|
+
protocolVersion: COMPOSITE_TRIGGER_PROTOCOL_VERSION,
|
|
389
|
+
schemaVersion: COMPOSITE_TRIGGER_SCHEMA_VERSION,
|
|
390
|
+
matchId: bufferId,
|
|
391
|
+
triggerId: definition.triggerId,
|
|
392
|
+
triggerVersion: definition.version,
|
|
393
|
+
status: 'partial',
|
|
394
|
+
correlationKey: key,
|
|
395
|
+
openedAt,
|
|
396
|
+
expiresAt: new Date(Date.parse(openedAt) + definition.withinMs).toISOString(),
|
|
397
|
+
updatedAt: nowIso,
|
|
398
|
+
sourceEvents,
|
|
399
|
+
correlationDecision: null,
|
|
400
|
+
patternState: {
|
|
401
|
+
version: '2',
|
|
402
|
+
role: 'buffer',
|
|
403
|
+
maxObservedOccurredAt: new Date(maxObserved).toISOString(),
|
|
404
|
+
semanticDecisions: [],
|
|
405
|
+
},
|
|
406
|
+
firedWakeId: null,
|
|
407
|
+
derivedEventIds: [],
|
|
408
|
+
});
|
|
409
|
+
await this.store.appendTriggerMatch(buffer);
|
|
410
|
+
return this.evaluatePatternV2Buffer(definition, buffer, now);
|
|
411
|
+
}
|
|
412
|
+
async evaluatePatternV2Buffer(definition, buffer, evaluationNow) {
|
|
413
|
+
const pattern = definition.patternV2;
|
|
414
|
+
if (!pattern)
|
|
415
|
+
return [];
|
|
416
|
+
const nowIso = evaluationNow.toISOString();
|
|
417
|
+
const evaluation = await evaluatePatternV2({
|
|
418
|
+
definition: pattern,
|
|
419
|
+
events: buffer.sourceEvents,
|
|
420
|
+
evaluator: this.evaluator,
|
|
421
|
+
now: evaluationNow,
|
|
422
|
+
allowedLatenessMs: definition.eventTime?.allowedLatenessMs ?? 0,
|
|
423
|
+
});
|
|
424
|
+
for (const semantic of evaluation.semanticTrace) {
|
|
425
|
+
await this.support.auditMatch(buffer, 'trigger.correlation', {
|
|
426
|
+
patternVersion: '2',
|
|
427
|
+
semanticNodeId: semantic.nodeId,
|
|
428
|
+
candidateSignature: semantic.candidateSignature,
|
|
429
|
+
evaluator: semantic.decision.evaluator,
|
|
430
|
+
outcome: semantic.decision.outcome,
|
|
431
|
+
probability: semantic.decision.probability,
|
|
432
|
+
matched: semantic.decision.matched,
|
|
433
|
+
shouldEscalate: semantic.decision.shouldEscalate,
|
|
434
|
+
inputFields: semantic.decision.condition.input,
|
|
435
|
+
...(semantic.decision.metadata
|
|
436
|
+
? { providerEvidence: semantic.decision.metadata }
|
|
437
|
+
: {}),
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
await this.support.cancelDeadlinesForMatch(buffer.matchId);
|
|
441
|
+
if (evaluation.pending.length) {
|
|
442
|
+
const deadlines = evaluation.pending
|
|
443
|
+
.filter((candidate) => candidate.pendingUntil)
|
|
444
|
+
.map((candidate) => ({
|
|
445
|
+
conditionId: `pattern-v2:${patternV2CandidateSignature(candidate)}`,
|
|
446
|
+
dueAt: candidate.pendingUntil,
|
|
447
|
+
}));
|
|
448
|
+
await this.support.scheduleDeadlines(definition, buffer, deadlines);
|
|
449
|
+
}
|
|
450
|
+
const previouslyEmitted = new Set(this.store
|
|
451
|
+
.listTriggerMatches(definition.triggerId)
|
|
452
|
+
.filter((record) => record.triggerVersion === definition.version &&
|
|
453
|
+
record.patternState?.version === '2' &&
|
|
454
|
+
record.patternState.role === 'match' &&
|
|
455
|
+
record.patternState.signature)
|
|
456
|
+
.map((record) => record.patternState.signature));
|
|
457
|
+
const fresh = evaluation.matches
|
|
458
|
+
.map((candidate, rank) => ({ candidate, rank }))
|
|
459
|
+
.filter(({ candidate }) => candidate.events.length > 0 &&
|
|
460
|
+
!previouslyEmitted.has(patternV2CandidateSignature(candidate)))
|
|
461
|
+
.sort((left, right) => left.candidate.startIndex - right.candidate.startIndex ||
|
|
462
|
+
left.rank - right.rank)
|
|
463
|
+
.map(({ candidate }) => candidate);
|
|
464
|
+
const selected = [];
|
|
465
|
+
const occupied = new Set();
|
|
466
|
+
let selectionLimit = pattern.selection.maxMatchesPerEvent;
|
|
467
|
+
const hasEffect = Boolean(definition.target || definition.derivedEvent);
|
|
468
|
+
if (hasEffect) {
|
|
469
|
+
const triggerState = this.store.getTriggerState?.(definition.triggerId, definition.version) ?? null;
|
|
470
|
+
const lifecycle = definition.lifecycle ?? {};
|
|
471
|
+
if (lifecycle.oneShot || lifecycle.completeOnGoal) {
|
|
472
|
+
selectionLimit = Math.min(selectionLimit, 1);
|
|
473
|
+
}
|
|
474
|
+
if (lifecycle.maxFirings !== undefined) {
|
|
475
|
+
selectionLimit = Math.min(selectionLimit, Math.max(0, lifecycle.maxFirings - (triggerState?.fireCount ?? 0)));
|
|
476
|
+
}
|
|
477
|
+
if ((lifecycle.cooldownMs ?? 0) > 0) {
|
|
478
|
+
selectionLimit = Math.min(selectionLimit, 1);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const indexed = [...buffer.sourceEvents].sort(sourceEventOrder);
|
|
482
|
+
const indexByIdentity = new Map(indexed.map((source, index) => [
|
|
483
|
+
patternPhysicalIdentity(source),
|
|
484
|
+
index,
|
|
485
|
+
]));
|
|
486
|
+
let skipPastLastThrough = -1;
|
|
487
|
+
let skipBefore = -1;
|
|
488
|
+
const skipStarts = new Set();
|
|
489
|
+
const bindingBoundary = (candidate, ref, edge) => {
|
|
490
|
+
const events = [...(candidate.bindings[ref] ?? [])]
|
|
491
|
+
.sort(sourceEventOrder);
|
|
492
|
+
const event = edge === 'first' ? events[0] : events.at(-1);
|
|
493
|
+
if (!event)
|
|
494
|
+
return null;
|
|
495
|
+
return indexByIdentity.get(patternPhysicalIdentity(event)) ?? null;
|
|
496
|
+
};
|
|
497
|
+
for (const candidate of fresh) {
|
|
498
|
+
if (selected.length >= selectionLimit)
|
|
499
|
+
break;
|
|
500
|
+
if (candidate.startIndex <= skipPastLastThrough)
|
|
501
|
+
continue;
|
|
502
|
+
if (candidate.startIndex < skipBefore)
|
|
503
|
+
continue;
|
|
504
|
+
if (skipStarts.has(candidate.startIndex))
|
|
505
|
+
continue;
|
|
506
|
+
const identities = candidate.events.map(patternPhysicalIdentity);
|
|
507
|
+
if (pattern.selection.overlap === 'disallow' &&
|
|
508
|
+
identities.some((identity) => occupied.has(identity))) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
selected.push(candidate);
|
|
512
|
+
identities.forEach((identity) => occupied.add(identity));
|
|
513
|
+
const afterMatch = pattern.selection.afterMatch;
|
|
514
|
+
if (afterMatch === 'skipPastLast') {
|
|
515
|
+
skipPastLastThrough = Math.max(skipPastLastThrough, candidate.endIndex);
|
|
516
|
+
}
|
|
517
|
+
else if (afterMatch === 'skipToNext') {
|
|
518
|
+
skipStarts.add(candidate.startIndex);
|
|
519
|
+
}
|
|
520
|
+
else if (typeof afterMatch === 'object') {
|
|
521
|
+
const boundary = bindingBoundary(candidate, afterMatch.ref, afterMatch.kind === 'skipToFirst' ? 'first' : 'last');
|
|
522
|
+
if (boundary !== null) {
|
|
523
|
+
skipBefore = Math.max(skipBefore, boundary);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const results = [];
|
|
528
|
+
for (const candidate of selected) {
|
|
529
|
+
const signature = patternV2CandidateSignature(candidate);
|
|
530
|
+
const matchId = `tm_${(await sha256Hex([
|
|
531
|
+
definition.triggerId,
|
|
532
|
+
definition.version,
|
|
533
|
+
buffer.correlationKey ?? '-',
|
|
534
|
+
signature,
|
|
535
|
+
'pattern-v2-match',
|
|
536
|
+
].join(':'))).slice(0, 24)}`;
|
|
537
|
+
const openedAt = candidate.events[0]?.occurredAt ?? buffer.openedAt;
|
|
538
|
+
const semanticDecisions = candidate.semanticDecisions.map((item) => this.patternDecisionRecord(item, nowIso));
|
|
539
|
+
const lastSemantic = semanticDecisions.at(-1)?.decision ?? null;
|
|
540
|
+
const matched = TriggerMatchRecordSchema.parse({
|
|
541
|
+
protocolVersion: COMPOSITE_TRIGGER_PROTOCOL_VERSION,
|
|
542
|
+
schemaVersion: COMPOSITE_TRIGGER_SCHEMA_VERSION,
|
|
543
|
+
matchId,
|
|
544
|
+
triggerId: definition.triggerId,
|
|
545
|
+
triggerVersion: definition.version,
|
|
546
|
+
status: 'matched',
|
|
547
|
+
correlationKey: buffer.correlationKey,
|
|
548
|
+
openedAt,
|
|
549
|
+
expiresAt: new Date(Date.parse(openedAt) + definition.withinMs).toISOString(),
|
|
550
|
+
updatedAt: nowIso,
|
|
551
|
+
sourceEvents: candidate.events,
|
|
552
|
+
correlationDecision: lastSemantic,
|
|
553
|
+
patternState: {
|
|
554
|
+
version: '2',
|
|
555
|
+
role: 'match',
|
|
556
|
+
signature,
|
|
557
|
+
semanticDecisions,
|
|
558
|
+
},
|
|
559
|
+
firedWakeId: null,
|
|
560
|
+
derivedEventIds: [],
|
|
561
|
+
});
|
|
562
|
+
await this.store.appendTriggerMatch(matched);
|
|
563
|
+
await this.support.auditMatch(matched, 'trigger.matched', {
|
|
564
|
+
patternVersion: '2',
|
|
565
|
+
signature,
|
|
566
|
+
semanticEvaluations: evaluation.semanticEvaluations,
|
|
567
|
+
truncated: evaluation.truncated,
|
|
568
|
+
sourceEventIds: matched.sourceEvents.map((source) => source.sourceEventId),
|
|
569
|
+
});
|
|
570
|
+
results.push({
|
|
571
|
+
triggerId: definition.triggerId,
|
|
572
|
+
match: matched,
|
|
573
|
+
matched: true,
|
|
574
|
+
fired: false,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
if (selected.length) {
|
|
578
|
+
const selectedIndexes = selected.flatMap((candidate) => [
|
|
579
|
+
candidate.startIndex,
|
|
580
|
+
candidate.endIndex,
|
|
581
|
+
]).filter((index) => index >= 0);
|
|
582
|
+
const firstStart = selectedIndexes.length
|
|
583
|
+
? Math.min(...selected.map((candidate) => candidate.startIndex))
|
|
584
|
+
: -1;
|
|
585
|
+
const lastEnd = selectedIndexes.length
|
|
586
|
+
? Math.max(...selected.map((candidate) => candidate.endIndex))
|
|
587
|
+
: -1;
|
|
588
|
+
let retained = [...buffer.sourceEvents];
|
|
589
|
+
const shouldDropByIndex = (index) => {
|
|
590
|
+
const afterMatch = pattern.selection.afterMatch;
|
|
591
|
+
if (afterMatch === 'skipPastLast') {
|
|
592
|
+
return index <= lastEnd;
|
|
593
|
+
}
|
|
594
|
+
if (afterMatch === 'skipToNext') {
|
|
595
|
+
return index <= firstStart;
|
|
596
|
+
}
|
|
597
|
+
if (typeof afterMatch === 'object') {
|
|
598
|
+
const boundaries = selected
|
|
599
|
+
.map((candidate) => bindingBoundary(candidate, afterMatch.ref, afterMatch.kind === 'skipToFirst' ? 'first' : 'last'))
|
|
600
|
+
.filter((value) => value !== null);
|
|
601
|
+
if (!boundaries.length)
|
|
602
|
+
return false;
|
|
603
|
+
return index < Math.max(...boundaries);
|
|
604
|
+
}
|
|
605
|
+
return false;
|
|
606
|
+
};
|
|
607
|
+
const consumed = pattern.selection.overlap === 'disallow'
|
|
608
|
+
? new Set(selected
|
|
609
|
+
.flatMap((candidate) => candidate.events)
|
|
610
|
+
.map(patternPhysicalIdentity))
|
|
611
|
+
: new Set();
|
|
612
|
+
retained = indexed.filter((source, index) => !shouldDropByIndex(index) &&
|
|
613
|
+
!consumed.has(patternPhysicalIdentity(source)));
|
|
614
|
+
const openedAt = retained[0]?.occurredAt ?? evaluationNow.toISOString();
|
|
615
|
+
const updatedBuffer = TriggerMatchRecordSchema.parse({
|
|
616
|
+
...buffer,
|
|
617
|
+
openedAt,
|
|
618
|
+
expiresAt: new Date(Date.parse(openedAt) + definition.withinMs).toISOString(),
|
|
619
|
+
updatedAt: nowIso,
|
|
620
|
+
sourceEvents: retained,
|
|
621
|
+
});
|
|
622
|
+
await this.store.appendTriggerMatch(updatedBuffer);
|
|
623
|
+
}
|
|
624
|
+
if (!results.length) {
|
|
625
|
+
await this.support.auditMatch(buffer, 'trigger.partial', {
|
|
626
|
+
reason: 'pattern_v2_wait',
|
|
627
|
+
patternVersion: '2',
|
|
628
|
+
pendingCandidates: evaluation.pending.length,
|
|
629
|
+
semanticEvaluations: evaluation.semanticEvaluations,
|
|
630
|
+
truncated: evaluation.truncated,
|
|
631
|
+
});
|
|
632
|
+
return [{
|
|
633
|
+
triggerId: definition.triggerId,
|
|
634
|
+
match: buffer,
|
|
635
|
+
matched: false,
|
|
636
|
+
fired: false,
|
|
637
|
+
}];
|
|
638
|
+
}
|
|
639
|
+
return results;
|
|
640
|
+
}
|
|
150
641
|
async applyEvent(definition, clause, event) {
|
|
151
642
|
const nowIso = this.now().toISOString();
|
|
152
643
|
const eventTime = Date.parse(event.occurredAt);
|
|
153
644
|
const key = deterministicKeyForClause(definition, clause.id, event);
|
|
154
|
-
const
|
|
645
|
+
const relatedMatches = this.store
|
|
155
646
|
.listTriggerMatches(definition.triggerId)
|
|
156
647
|
.filter((record) => record.triggerVersion === definition.version &&
|
|
157
|
-
(record
|
|
158
|
-
|
|
648
|
+
matchesCorrelationKey(record, key));
|
|
649
|
+
const activeMatches = relatedMatches.filter((record) => record.status === 'partial' || record.status === 'matched');
|
|
650
|
+
const alreadyMatched = activeMatches
|
|
651
|
+
.filter((record) => record.status === 'matched')
|
|
159
652
|
.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0];
|
|
160
|
-
if (
|
|
653
|
+
if (alreadyMatched) {
|
|
161
654
|
return {
|
|
162
655
|
triggerId: definition.triggerId,
|
|
163
|
-
match:
|
|
656
|
+
match: alreadyMatched,
|
|
164
657
|
matched: true,
|
|
165
658
|
fired: false,
|
|
166
659
|
};
|
|
167
660
|
}
|
|
168
|
-
|
|
169
|
-
|
|
661
|
+
const partials = activeMatches.filter((record) => record.status === 'partial');
|
|
662
|
+
const observedEventTimes = relatedMatches.flatMap((record) => record.sourceEvents.map((source) => Date.parse(source.occurredAt)));
|
|
663
|
+
const maxObservedEventTime = Math.max(eventTime, ...observedEventTimes.filter(Number.isFinite));
|
|
664
|
+
const allowedLatenessMs = definition.eventTime?.allowedLatenessMs ?? 0;
|
|
665
|
+
const watermarkMs = maxObservedEventTime - allowedLatenessMs;
|
|
666
|
+
// The watermark is the point before which new standalone windows are too
|
|
667
|
+
// late to open. Existing partials are retained until their event-time
|
|
668
|
+
// expiry falls strictly behind it, allowing a late event to complete a
|
|
669
|
+
// still-valid in-window pattern.
|
|
670
|
+
const staleIds = new Set();
|
|
671
|
+
for (const candidate of partials) {
|
|
672
|
+
if (Date.parse(candidate.expiresAt) >= watermarkMs)
|
|
673
|
+
continue;
|
|
674
|
+
staleIds.add(candidate.matchId);
|
|
170
675
|
const expired = TriggerMatchRecordSchema.parse({
|
|
171
|
-
...
|
|
676
|
+
...candidate,
|
|
172
677
|
status: 'expired',
|
|
173
678
|
updatedAt: nowIso,
|
|
174
679
|
});
|
|
175
680
|
await this.store.appendTriggerMatch(expired);
|
|
176
681
|
await this.support.cancelDeadlinesForMatch(expired.matchId);
|
|
177
682
|
await this.support.auditMatch(expired, 'trigger.expired', {
|
|
178
|
-
reason: '
|
|
683
|
+
reason: 'event_time_watermark_advanced',
|
|
684
|
+
watermarkAt: new Date(watermarkMs).toISOString(),
|
|
685
|
+
allowedLatenessMs,
|
|
686
|
+
incomingSourceEventId: event.sourceEventId,
|
|
687
|
+
incomingOccurredAt: event.occurredAt,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
const accumulatesOccurrences = clauseAccumulatesOccurrences(definition, clause.id);
|
|
691
|
+
const compatible = partials
|
|
692
|
+
.filter((record) => !staleIds.has(record.matchId))
|
|
693
|
+
.map((record) => ({
|
|
694
|
+
record,
|
|
695
|
+
window: prospectiveEventWindow(record, event, definition.withinMs),
|
|
696
|
+
hasClause: record.sourceEvents.some((source) => source.clauseId === clause.id),
|
|
697
|
+
}))
|
|
698
|
+
.filter(({ window }) => window.fits)
|
|
699
|
+
.sort((a, b) => (accumulatesOccurrences
|
|
700
|
+
? 0
|
|
701
|
+
: Number(a.hasClause) - Number(b.hasClause)) ||
|
|
702
|
+
a.window.spanMs - b.window.spanMs ||
|
|
703
|
+
Date.parse(b.record.updatedAt) - Date.parse(a.record.updatedAt));
|
|
704
|
+
let record = compatible[0]?.record;
|
|
705
|
+
if (record &&
|
|
706
|
+
!accumulatesOccurrences &&
|
|
707
|
+
compatible[0]?.hasClause) {
|
|
708
|
+
// Repeated non-aggregating occurrences represent alternate paths through
|
|
709
|
+
// the same partial match. Branch the candidate so the new occurrence can
|
|
710
|
+
// advance the expression without destroying the pre-existing window.
|
|
711
|
+
record = await this.branchMatch(definition, record, event);
|
|
712
|
+
}
|
|
713
|
+
if (!record && eventTime < watermarkMs) {
|
|
714
|
+
await this.store.appendAudit({
|
|
715
|
+
auditId: `audit_${(await sha256Hex(`${definition.triggerId}:${definition.version}:late_event_dropped:${event.sourceEventId}:${nowIso}`)).slice(0, 24)}`,
|
|
716
|
+
traceId: event.traceId,
|
|
717
|
+
timestamp: nowIso,
|
|
718
|
+
kind: 'event.late_dropped',
|
|
719
|
+
entityType: 'trigger',
|
|
720
|
+
entityId: definition.triggerId,
|
|
721
|
+
details: {
|
|
722
|
+
triggerVersion: definition.version,
|
|
723
|
+
sourceEventId: event.sourceEventId,
|
|
724
|
+
occurredAt: event.occurredAt,
|
|
725
|
+
receivedAt: event.receivedAt ?? null,
|
|
726
|
+
watermarkAt: new Date(watermarkMs).toISOString(),
|
|
727
|
+
allowedLatenessMs,
|
|
728
|
+
},
|
|
179
729
|
});
|
|
180
|
-
|
|
730
|
+
return {
|
|
731
|
+
triggerId: definition.triggerId,
|
|
732
|
+
match: null,
|
|
733
|
+
matched: false,
|
|
734
|
+
fired: false,
|
|
735
|
+
};
|
|
181
736
|
}
|
|
737
|
+
record ??= await this.newMatch(definition, key, event);
|
|
182
738
|
const duplicate = record.sourceEvents.some((candidate) => sameEventIdentity(candidate, event));
|
|
183
739
|
if (!duplicate) {
|
|
740
|
+
const window = prospectiveEventWindow(record, event, definition.withinMs);
|
|
741
|
+
if (!window.fits) {
|
|
742
|
+
// This can only happen for a newly-created incompatible candidate race.
|
|
743
|
+
// Keep the existing partial intact and seed a fresh event-time window.
|
|
744
|
+
record = await this.newMatch(definition, key, event);
|
|
745
|
+
}
|
|
746
|
+
const selectedWindow = prospectiveEventWindow(record, event, definition.withinMs);
|
|
184
747
|
record = TriggerMatchRecordSchema.parse({
|
|
185
748
|
...record,
|
|
186
749
|
correlationKey: record.correlationKey ?? key,
|
|
750
|
+
openedAt: selectedWindow.openedAt,
|
|
751
|
+
expiresAt: selectedWindow.expiresAt,
|
|
187
752
|
updatedAt: nowIso,
|
|
188
753
|
sourceEvents: [
|
|
189
754
|
...record.sourceEvents,
|
|
@@ -196,6 +761,12 @@ export class CompositeTriggerEngine {
|
|
|
196
761
|
await this.support.auditMatch(record, 'trigger.partial', {
|
|
197
762
|
clauseId: clause.id,
|
|
198
763
|
sourceEventId: event.sourceEventId,
|
|
764
|
+
occurredAt: event.occurredAt,
|
|
765
|
+
receivedAt: event.receivedAt ?? null,
|
|
766
|
+
eventTimeWindow: {
|
|
767
|
+
openedAt: record.openedAt,
|
|
768
|
+
expiresAt: record.expiresAt,
|
|
769
|
+
},
|
|
199
770
|
});
|
|
200
771
|
return {
|
|
201
772
|
triggerId: definition.triggerId,
|
|
@@ -315,6 +886,24 @@ export class CompositeTriggerEngine {
|
|
|
315
886
|
fired: false,
|
|
316
887
|
};
|
|
317
888
|
}
|
|
889
|
+
async branchMatch(definition, source, event) {
|
|
890
|
+
const matchId = `tm_${(await sha256Hex([
|
|
891
|
+
definition.triggerId,
|
|
892
|
+
definition.version,
|
|
893
|
+
source.matchId,
|
|
894
|
+
event.sourceEventId,
|
|
895
|
+
'branch',
|
|
896
|
+
].join(':'))).slice(0, 24)}`;
|
|
897
|
+
return TriggerMatchRecordSchema.parse({
|
|
898
|
+
...source,
|
|
899
|
+
matchId,
|
|
900
|
+
status: 'partial',
|
|
901
|
+
updatedAt: this.now().toISOString(),
|
|
902
|
+
correlationDecision: null,
|
|
903
|
+
firedWakeId: null,
|
|
904
|
+
derivedEventIds: [],
|
|
905
|
+
});
|
|
906
|
+
}
|
|
318
907
|
async newMatch(definition, key, event) {
|
|
319
908
|
const openedAt = event.occurredAt;
|
|
320
909
|
const expiresAt = new Date(Date.parse(openedAt) + definition.withinMs).toISOString();
|
|
@@ -4,5 +4,5 @@ export declare function asSourceEvent(clauseId: string, event: CorrelatableEvent
|
|
|
4
4
|
export declare function expressionSatisfied(definition: CompositeTriggerDefinition, record: TriggerMatchRecord): boolean;
|
|
5
5
|
export declare function semanticSource(record: TriggerMatchRecord): Record<string, unknown>;
|
|
6
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;
|
|
7
|
+
export declare function sameEventIdentity(source: Pick<TriggerSourceEvent, 'sourceEventId' | 'serverId' | 'subscriptionArguments'>, event: Pick<CorrelatableEvent, 'sourceEventId' | 'serverId' | 'subscriptionArguments'>): boolean;
|
|
8
8
|
export declare function matchesCorrelationKey(record: TriggerMatchRecord, key: string | null): boolean;
|