mcp-event-intelligence 0.5.0 → 0.6.1
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 +53 -0
- package/README.md +31 -38
- package/SECURITY.md +4 -4
- package/bin/mcp-event-intelligence.mjs +2 -2
- package/dist/src/composite/engine.d.ts +11 -7
- package/dist/src/composite/engine.js +152 -414
- package/dist/src/composite/matching.d.ts +1 -5
- package/dist/src/composite/matching.js +0 -54
- package/dist/src/composite/runtimeSupport.js +10 -6
- package/dist/src/composite/store.d.ts +38 -4
- package/dist/src/composite/temporal.d.ts +0 -17
- package/dist/src/composite/temporal.js +0 -266
- package/dist/src/intelligenceProtocol/activationSchemas.d.ts +36 -3
- package/dist/src/intelligenceProtocol/activationSchemas.js +4 -3
- package/dist/src/intelligenceProtocol/index.d.ts +8 -12
- package/dist/src/intelligenceProtocol/index.js +4 -7
- package/dist/src/intelligenceProtocol/lifecycle.js +2 -10
- package/dist/src/intelligenceProtocol/operatorRegistry.d.ts +3 -270
- package/dist/src/intelligenceProtocol/operatorRegistry.js +5 -144
- package/dist/src/intelligenceProtocol/schemas.d.ts +0 -189
- package/dist/src/intelligenceProtocol/schemas.js +1 -97
- package/dist/src/intelligenceProtocol/triggerPlanSchemas.d.ts +5 -102
- package/dist/src/intelligenceProtocol/triggerPlanSchemas.js +2 -26
- package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +14 -215
- package/dist/src/intelligenceProtocol/triggerSchemas.js +18 -191
- package/dist/src/intelligenceProtocol/version.d.ts +2 -2
- package/dist/src/intelligenceProtocol/version.js +2 -2
- package/dist/src/patternV2/evaluator.d.ts +9 -1
- package/dist/src/patternV2/evaluator.js +63 -4
- package/dist/src/semantic/types.d.ts +6 -0
- package/dist/src/semantic/typesafeJevEvaluator.d.ts +1 -0
- package/dist/src/semantic/typesafeJevEvaluator.js +6 -0
- package/package.json +5 -2
- package/scripts/host-integration.d.mts +48 -5
- package/scripts/host-integration.mjs +16 -4
- package/scripts/lib/activation-hydrator.mjs +12 -12
- package/scripts/lib/composite-event-consumer.mjs +8 -28
- package/scripts/lib/composite-wake-coordinator.mjs +19 -19
- package/scripts/lib/derived-contract-registry.mjs +4 -4
- package/scripts/lib/derived-event-coordinator.mjs +11 -22
- package/scripts/lib/local-event-intelligence-runtime.mjs +35 -3
- package/scripts/lib/mcp-events-client.mjs +36 -32
- package/scripts/lib/persistent-event-store.mjs +228 -61
- package/scripts/lib/store-capabilities.mjs +88 -0
- package/scripts/lib/store-conformance.mjs +152 -0
- package/scripts/lib/temporal-deadline-scheduler.mjs +3 -3
- package/scripts/lib/trigger-control-plane.mjs +52 -57
- package/scripts/lib/trigger-inspector.mjs +48 -160
- package/scripts/lib/trigger-planner.mjs +37 -126
- package/scripts/lib/wake-retry-scheduler.mjs +2 -19
- package/scripts/mcp-stdio-server.mjs +8 -8
- package/scripts/service.mjs +19 -138
- package/server.json +3 -3
- package/dist/src/intelligenceProtocol/invariants.d.ts +0 -3
- package/dist/src/intelligenceProtocol/invariants.js +0 -36
- package/dist/src/intelligenceProtocol/mapper.d.ts +0 -14
- package/dist/src/intelligenceProtocol/mapper.js +0 -28
- package/dist/src/patternV2/legacyCompiler.d.ts +0 -3
- package/dist/src/patternV2/legacyCompiler.js +0 -184
- package/scripts/lib/event-processor.mjs +0 -624
- package/scripts/lib/http-wake-deliverer.mjs +0 -31
|
@@ -1,24 +1,9 @@
|
|
|
1
|
-
import { SemanticConditionEngine } from '../semantic/conditionEngine.js';
|
|
2
1
|
import { evaluatePatternV2, patternV2CandidateSignature, } from '../patternV2/evaluator.js';
|
|
3
2
|
import { sha256Hex } from '../intelligenceProtocol/canonical.js';
|
|
4
3
|
import { COMPOSITE_TRIGGER_PROTOCOL_VERSION, COMPOSITE_TRIGGER_SCHEMA_VERSION, CompositeTriggerDefinitionSchema, CorrelatableEventSchema, TriggerMatchRecordSchema, } from '../intelligenceProtocol/triggerSchemas.js';
|
|
5
|
-
import {
|
|
6
|
-
import { asSourceEvent, clauseMatches, deterministicKeyForClause, expressionSatisfied, matchesCorrelationKey, sameEventIdentity, semanticSource, } from './matching.js';
|
|
4
|
+
import { asSourceEvent, clauseMatches, sameEventIdentity, } from './matching.js';
|
|
7
5
|
import { assertNoDerivedEventCycle } from './derivedGraph.js';
|
|
8
6
|
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
7
|
function getByPath(source, path) {
|
|
23
8
|
let current = source;
|
|
24
9
|
for (const part of path.split('.')) {
|
|
@@ -55,35 +40,32 @@ function patternPhysicalIdentity(event) {
|
|
|
55
40
|
JSON.stringify(stablePatternValue(event.subscriptionArguments ?? {})),
|
|
56
41
|
].join(':');
|
|
57
42
|
}
|
|
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
|
-
}
|
|
73
43
|
export class CompositeTriggerEngine {
|
|
74
44
|
store;
|
|
75
45
|
evaluator;
|
|
76
46
|
now;
|
|
77
47
|
support;
|
|
78
|
-
|
|
48
|
+
workerId;
|
|
49
|
+
partitionLeaseMs;
|
|
50
|
+
constructor(store, evaluator = null, now = () => new Date(), options = {}) {
|
|
79
51
|
this.store = store;
|
|
80
52
|
this.evaluator = evaluator;
|
|
81
53
|
this.now = now;
|
|
82
54
|
this.support = new CompositeTriggerRuntimeSupport(store, now);
|
|
55
|
+
this.workerId =
|
|
56
|
+
String(options.workerId || 'event-intelligence-engine');
|
|
57
|
+
this.partitionLeaseMs = Math.max(1000, Number(options.partitionLeaseMs) || 300000);
|
|
83
58
|
}
|
|
84
59
|
async register(definitionInput) {
|
|
85
60
|
const definition = CompositeTriggerDefinitionSchema.parse(definitionInput);
|
|
86
|
-
|
|
61
|
+
const existingDefinitions = await this.store.listTriggers();
|
|
62
|
+
const stateByTrigger = new Map();
|
|
63
|
+
if (this.store.getTriggerState) {
|
|
64
|
+
await Promise.all(existingDefinitions.map(async (existing) => {
|
|
65
|
+
stateByTrigger.set(`${existing.triggerId}@${existing.version}`, await this.store.getTriggerState(existing.triggerId, existing.version));
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
assertNoDerivedEventCycle(existingDefinitions, definition, (triggerId, version) => stateByTrigger.get(`${triggerId}@${version}`) ?? null);
|
|
87
69
|
await this.store.putTrigger(definition);
|
|
88
70
|
return definition;
|
|
89
71
|
}
|
|
@@ -93,49 +75,33 @@ export class CompositeTriggerEngine {
|
|
|
93
75
|
return this.ingestTimerEvent(event);
|
|
94
76
|
}
|
|
95
77
|
const results = [];
|
|
96
|
-
for (const definition of this.store.listTriggers()) {
|
|
97
|
-
const triggerState = this.store.getTriggerState
|
|
78
|
+
for (const definition of await this.store.listTriggers()) {
|
|
79
|
+
const triggerState = this.store.getTriggerState
|
|
80
|
+
? await this.store.getTriggerState(definition.triggerId, definition.version)
|
|
81
|
+
: null;
|
|
98
82
|
if (!(await this.support.lifecycleAllows(definition, triggerState, this.now()))) {
|
|
99
83
|
continue;
|
|
100
84
|
}
|
|
101
85
|
const matchingClauses = definition.clauses.filter((clause) => clauseMatches(clause, event));
|
|
102
86
|
if (matchingClauses.length === 0)
|
|
103
87
|
continue;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
}
|
|
136
|
-
const replayed = this.store
|
|
137
|
-
.listTriggerMatches(definition.triggerId)
|
|
138
|
-
.find((record) => record.sourceEvents.some((source) => sameEventIdentity(source, event)));
|
|
88
|
+
const replayed = (await this.store
|
|
89
|
+
.listTriggerMatches(definition.triggerId))
|
|
90
|
+
.filter((record) => record.triggerVersion === definition.version &&
|
|
91
|
+
record.sourceEvents.some((source) => sameEventIdentity(source, event)))
|
|
92
|
+
.sort((a, b) => {
|
|
93
|
+
const rank = (status) => status === 'fired'
|
|
94
|
+
? 4
|
|
95
|
+
: status === 'emitted'
|
|
96
|
+
? 3
|
|
97
|
+
: status === 'matched'
|
|
98
|
+
? 2
|
|
99
|
+
: status === 'partial'
|
|
100
|
+
? 1
|
|
101
|
+
: 0;
|
|
102
|
+
return (rank(b.status) - rank(a.status) ||
|
|
103
|
+
Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
|
|
104
|
+
})[0];
|
|
139
105
|
if (replayed) {
|
|
140
106
|
results.push({
|
|
141
107
|
triggerId: definition.triggerId,
|
|
@@ -147,13 +113,12 @@ export class CompositeTriggerEngine {
|
|
|
147
113
|
});
|
|
148
114
|
continue;
|
|
149
115
|
}
|
|
150
|
-
|
|
151
|
-
results.push(await this.applyEvent(definition, clause, event));
|
|
116
|
+
results.push(...await this.applyPattern(definition, matchingClauses, event));
|
|
152
117
|
}
|
|
153
118
|
return results;
|
|
154
119
|
}
|
|
155
120
|
async markFired(matchId, wakeId) {
|
|
156
|
-
const latest = this.latestMatch(matchId);
|
|
121
|
+
const latest = await this.latestMatch(matchId);
|
|
157
122
|
if (!latest)
|
|
158
123
|
throw new Error(`Unknown trigger match: ${matchId}`);
|
|
159
124
|
if (latest.status === 'fired')
|
|
@@ -174,7 +139,7 @@ export class CompositeTriggerEngine {
|
|
|
174
139
|
return fired;
|
|
175
140
|
}
|
|
176
141
|
async markDerivedEmitted(matchId, derivedEventId) {
|
|
177
|
-
const latest = this.latestMatch(matchId);
|
|
142
|
+
const latest = await this.latestMatch(matchId);
|
|
178
143
|
if (!latest)
|
|
179
144
|
throw new Error(`Unknown trigger match: ${matchId}`);
|
|
180
145
|
if (latest.status === 'emitted' &&
|
|
@@ -204,18 +169,20 @@ export class CompositeTriggerEngine {
|
|
|
204
169
|
const deadlineId = String(event.data.deadlineId ?? '');
|
|
205
170
|
if (!deadlineId || !this.store.getTemporalDeadline)
|
|
206
171
|
return [];
|
|
207
|
-
const deadline = this.store.getTemporalDeadline(deadlineId);
|
|
172
|
+
const deadline = await this.store.getTemporalDeadline(deadlineId);
|
|
208
173
|
if (!deadline || deadline.status !== 'pending')
|
|
209
174
|
return [];
|
|
210
|
-
const definition = this.store.listTriggers().find((candidate) => candidate.triggerId === deadline.triggerId &&
|
|
175
|
+
const definition = (await this.store.listTriggers()).find((candidate) => candidate.triggerId === deadline.triggerId &&
|
|
211
176
|
candidate.version === deadline.triggerVersion);
|
|
212
177
|
if (!definition)
|
|
213
178
|
return [];
|
|
214
|
-
const triggerState = this.store.getTriggerState
|
|
179
|
+
const triggerState = this.store.getTriggerState
|
|
180
|
+
? await this.store.getTriggerState(definition.triggerId, definition.version)
|
|
181
|
+
: null;
|
|
215
182
|
if (!(await this.support.lifecycleAllows(definition, triggerState, this.now()))) {
|
|
216
183
|
return [];
|
|
217
184
|
}
|
|
218
|
-
const match = this.latestMatch(deadline.matchId);
|
|
185
|
+
const match = await this.latestMatch(deadline.matchId);
|
|
219
186
|
if (!match ||
|
|
220
187
|
match.status === 'matched' ||
|
|
221
188
|
match.status === 'emitted' ||
|
|
@@ -223,34 +190,14 @@ export class CompositeTriggerEngine {
|
|
|
223
190
|
match.status === 'expired') {
|
|
224
191
|
return [];
|
|
225
192
|
}
|
|
226
|
-
if (
|
|
227
|
-
match.patternState?.version === '2' &&
|
|
193
|
+
if (match.patternState?.version === '2' &&
|
|
228
194
|
match.patternState.role === 'buffer') {
|
|
229
|
-
return this.
|
|
230
|
-
}
|
|
231
|
-
if (Date.parse(event.occurredAt) > Date.parse(match.expiresAt)) {
|
|
232
|
-
const expired = TriggerMatchRecordSchema.parse({
|
|
233
|
-
...match,
|
|
234
|
-
status: 'expired',
|
|
235
|
-
updatedAt: event.occurredAt,
|
|
236
|
-
});
|
|
237
|
-
await this.store.appendTriggerMatch(expired);
|
|
238
|
-
await this.support.cancelDeadlinesForMatch(expired.matchId);
|
|
239
|
-
await this.support.auditMatch(expired, 'trigger.expired', {
|
|
240
|
-
reason: 'temporal_deadline_after_event_window',
|
|
241
|
-
});
|
|
242
|
-
return [{
|
|
243
|
-
triggerId: definition.triggerId,
|
|
244
|
-
match: expired,
|
|
245
|
-
matched: false,
|
|
246
|
-
fired: false,
|
|
247
|
-
}];
|
|
195
|
+
return this.evaluatePatternBuffer(definition, match, new Date(event.occurredAt));
|
|
248
196
|
}
|
|
249
|
-
|
|
250
|
-
return [result];
|
|
197
|
+
return [];
|
|
251
198
|
}
|
|
252
199
|
patternPartitionKey(definition, matchingClauses, event) {
|
|
253
|
-
const dimensions = definition.
|
|
200
|
+
const dimensions = definition.pattern?.partitionBy ?? [];
|
|
254
201
|
if (!dimensions.length)
|
|
255
202
|
return null;
|
|
256
203
|
const clauseIds = new Set(matchingClauses.map((clause) => clause.id));
|
|
@@ -305,15 +252,55 @@ export class CompositeTriggerEngine {
|
|
|
305
252
|
},
|
|
306
253
|
};
|
|
307
254
|
}
|
|
308
|
-
async
|
|
309
|
-
const
|
|
255
|
+
async applyPattern(definition, matchingClauses, event) {
|
|
256
|
+
const key = this.patternPartitionKey(definition, matchingClauses, event);
|
|
257
|
+
const capabilities = this.store.storeCapabilities?.();
|
|
258
|
+
const distributedLeases = capabilities?.partitionLeases === 'distributed-atomic';
|
|
259
|
+
if (!distributedLeases) {
|
|
260
|
+
return this.applyPatternWithPartition(definition, matchingClauses, event, key);
|
|
261
|
+
}
|
|
262
|
+
if (!this.store.claimPartitionLease ||
|
|
263
|
+
!this.store.releasePartitionLease) {
|
|
264
|
+
const error = new Error('Distributed Pattern v2 store is missing partition lease methods');
|
|
265
|
+
error.code =
|
|
266
|
+
'PATTERN_PARTITION_LEASE_UNSUPPORTED';
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
const leaseKey = [
|
|
270
|
+
definition.triggerId,
|
|
271
|
+
definition.version,
|
|
272
|
+
key ?? '-',
|
|
273
|
+
].join(':');
|
|
274
|
+
const nowIso = this.now().toISOString();
|
|
275
|
+
const lease = await this.store.claimPartitionLease(leaseKey, {
|
|
276
|
+
workerId: this.workerId,
|
|
277
|
+
now: nowIso,
|
|
278
|
+
leaseMs: this.partitionLeaseMs,
|
|
279
|
+
});
|
|
280
|
+
if (!lease) {
|
|
281
|
+
const error = new Error(`Pattern partition is currently owned by another worker: ${leaseKey}`);
|
|
282
|
+
error.code =
|
|
283
|
+
'PATTERN_PARTITION_LEASE_BUSY';
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
return await this.applyPatternWithPartition(definition, matchingClauses, event, key);
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
await this.store.releasePartitionLease(leaseKey, {
|
|
291
|
+
workerId: this.workerId,
|
|
292
|
+
now: this.now().toISOString(),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async applyPatternWithPartition(definition, matchingClauses, event, key) {
|
|
297
|
+
const pattern = definition.pattern;
|
|
310
298
|
if (!pattern)
|
|
311
299
|
return [];
|
|
312
300
|
const now = this.now();
|
|
313
301
|
const nowIso = now.toISOString();
|
|
314
|
-
const key = this.patternPartitionKey(definition, matchingClauses, event);
|
|
315
302
|
const bufferId = await this.patternBufferId(definition, key);
|
|
316
|
-
const existing = this.latestMatch(bufferId);
|
|
303
|
+
const existing = await this.latestMatch(bufferId);
|
|
317
304
|
let sourceEvents = existing?.sourceEvents
|
|
318
305
|
? [...existing.sourceEvents]
|
|
319
306
|
: [];
|
|
@@ -348,7 +335,7 @@ export class CompositeTriggerEngine {
|
|
|
348
335
|
details: {
|
|
349
336
|
triggerVersion: definition.version,
|
|
350
337
|
patternVersion: '2',
|
|
351
|
-
|
|
338
|
+
partitionKey: key,
|
|
352
339
|
bufferedEvents: bufferedPhysicalEvents,
|
|
353
340
|
maxBufferedEvents: pattern.execution.maxBufferedEvents,
|
|
354
341
|
},
|
|
@@ -391,12 +378,12 @@ export class CompositeTriggerEngine {
|
|
|
391
378
|
triggerId: definition.triggerId,
|
|
392
379
|
triggerVersion: definition.version,
|
|
393
380
|
status: 'partial',
|
|
394
|
-
|
|
381
|
+
partitionKey: key,
|
|
395
382
|
openedAt,
|
|
396
383
|
expiresAt: new Date(Date.parse(openedAt) + definition.withinMs).toISOString(),
|
|
397
384
|
updatedAt: nowIso,
|
|
398
385
|
sourceEvents,
|
|
399
|
-
|
|
386
|
+
semanticDecision: null,
|
|
400
387
|
patternState: {
|
|
401
388
|
version: '2',
|
|
402
389
|
role: 'buffer',
|
|
@@ -407,19 +394,45 @@ export class CompositeTriggerEngine {
|
|
|
407
394
|
derivedEventIds: [],
|
|
408
395
|
});
|
|
409
396
|
await this.store.appendTriggerMatch(buffer);
|
|
410
|
-
return this.
|
|
397
|
+
return this.evaluatePatternBuffer(definition, buffer, now);
|
|
411
398
|
}
|
|
412
|
-
|
|
413
|
-
|
|
399
|
+
candidateFitsTriggerWindow(candidate, withinMs) {
|
|
400
|
+
if (candidate.events.length < 2)
|
|
401
|
+
return true;
|
|
402
|
+
const times = candidate.events
|
|
403
|
+
.map((event) => Date.parse(event.occurredAt))
|
|
404
|
+
.filter(Number.isFinite);
|
|
405
|
+
if (times.length < 2)
|
|
406
|
+
return true;
|
|
407
|
+
return Math.max(...times) - Math.min(...times) <= withinMs;
|
|
408
|
+
}
|
|
409
|
+
async evaluatePatternBuffer(definition, buffer, evaluationNow) {
|
|
410
|
+
const pattern = definition.pattern;
|
|
414
411
|
if (!pattern)
|
|
415
412
|
return [];
|
|
416
413
|
const nowIso = evaluationNow.toISOString();
|
|
414
|
+
const semanticCache = typeof this.store.getSemanticDecisionCache === 'function' &&
|
|
415
|
+
typeof this.store.putSemanticDecisionCache === 'function'
|
|
416
|
+
? {
|
|
417
|
+
get: (key) => this.store.getSemanticDecisionCache(key),
|
|
418
|
+
set: (key, decision) => this.store.putSemanticDecisionCache(key, decision),
|
|
419
|
+
}
|
|
420
|
+
: null;
|
|
421
|
+
const reachedDeadlines = this.store.listTemporalDeadlines
|
|
422
|
+
? (await this.store.listTemporalDeadlines({
|
|
423
|
+
matchId: buffer.matchId,
|
|
424
|
+
status: 'pending',
|
|
425
|
+
}))
|
|
426
|
+
.filter((deadline) => Date.parse(deadline.dueAt) <= evaluationNow.getTime())
|
|
427
|
+
: [];
|
|
417
428
|
const evaluation = await evaluatePatternV2({
|
|
418
429
|
definition: pattern,
|
|
419
430
|
events: buffer.sourceEvents,
|
|
420
431
|
evaluator: this.evaluator,
|
|
421
432
|
now: evaluationNow,
|
|
422
433
|
allowedLatenessMs: definition.eventTime?.allowedLatenessMs ?? 0,
|
|
434
|
+
semanticCache,
|
|
435
|
+
semanticCacheNamespace: `${definition.triggerId}@${definition.version}`,
|
|
423
436
|
});
|
|
424
437
|
for (const semantic of evaluation.semanticTrace) {
|
|
425
438
|
await this.support.auditMatch(buffer, 'trigger.correlation', {
|
|
@@ -431,6 +444,8 @@ export class CompositeTriggerEngine {
|
|
|
431
444
|
probability: semantic.decision.probability,
|
|
432
445
|
matched: semantic.decision.matched,
|
|
433
446
|
shouldEscalate: semantic.decision.shouldEscalate,
|
|
447
|
+
semanticCacheHits: evaluation.semanticCacheHits,
|
|
448
|
+
semanticCacheMisses: evaluation.semanticCacheMisses,
|
|
434
449
|
inputFields: semantic.decision.condition.input,
|
|
435
450
|
...(semantic.decision.metadata
|
|
436
451
|
? { providerEvidence: semantic.decision.metadata }
|
|
@@ -440,21 +455,23 @@ export class CompositeTriggerEngine {
|
|
|
440
455
|
await this.support.cancelDeadlinesForMatch(buffer.matchId);
|
|
441
456
|
if (evaluation.pending.length) {
|
|
442
457
|
const deadlines = evaluation.pending
|
|
443
|
-
.filter((candidate) => candidate.pendingUntil
|
|
458
|
+
.filter((candidate) => candidate.pendingUntil &&
|
|
459
|
+
this.candidateFitsTriggerWindow(candidate, definition.withinMs))
|
|
444
460
|
.map((candidate) => ({
|
|
445
461
|
conditionId: `pattern-v2:${patternV2CandidateSignature(candidate)}`,
|
|
446
462
|
dueAt: candidate.pendingUntil,
|
|
447
463
|
}));
|
|
448
464
|
await this.support.scheduleDeadlines(definition, buffer, deadlines);
|
|
449
465
|
}
|
|
450
|
-
const previouslyEmitted = new Set(this.store
|
|
451
|
-
.listTriggerMatches(definition.triggerId)
|
|
466
|
+
const previouslyEmitted = new Set((await this.store
|
|
467
|
+
.listTriggerMatches(definition.triggerId))
|
|
452
468
|
.filter((record) => record.triggerVersion === definition.version &&
|
|
453
469
|
record.patternState?.version === '2' &&
|
|
454
470
|
record.patternState.role === 'match' &&
|
|
455
471
|
record.patternState.signature)
|
|
456
472
|
.map((record) => record.patternState.signature));
|
|
457
473
|
const fresh = evaluation.matches
|
|
474
|
+
.filter((candidate) => this.candidateFitsTriggerWindow(candidate, definition.withinMs))
|
|
458
475
|
.map((candidate, rank) => ({ candidate, rank }))
|
|
459
476
|
.filter(({ candidate }) => candidate.events.length > 0 &&
|
|
460
477
|
!previouslyEmitted.has(patternV2CandidateSignature(candidate)))
|
|
@@ -466,7 +483,9 @@ export class CompositeTriggerEngine {
|
|
|
466
483
|
let selectionLimit = pattern.selection.maxMatchesPerEvent;
|
|
467
484
|
const hasEffect = Boolean(definition.target || definition.derivedEvent);
|
|
468
485
|
if (hasEffect) {
|
|
469
|
-
const triggerState = this.store.getTriggerState
|
|
486
|
+
const triggerState = this.store.getTriggerState
|
|
487
|
+
? await this.store.getTriggerState(definition.triggerId, definition.version)
|
|
488
|
+
: null;
|
|
470
489
|
const lifecycle = definition.lifecycle ?? {};
|
|
471
490
|
if (lifecycle.oneShot || lifecycle.completeOnGoal) {
|
|
472
491
|
selectionLimit = Math.min(selectionLimit, 1);
|
|
@@ -530,12 +549,23 @@ export class CompositeTriggerEngine {
|
|
|
530
549
|
const matchId = `tm_${(await sha256Hex([
|
|
531
550
|
definition.triggerId,
|
|
532
551
|
definition.version,
|
|
533
|
-
buffer.
|
|
552
|
+
buffer.partitionKey ?? '-',
|
|
534
553
|
signature,
|
|
535
554
|
'pattern-v2-match',
|
|
536
555
|
].join(':'))).slice(0, 24)}`;
|
|
537
556
|
const openedAt = candidate.events[0]?.occurredAt ?? buffer.openedAt;
|
|
538
557
|
const semanticDecisions = candidate.semanticDecisions.map((item) => this.patternDecisionRecord(item, nowIso));
|
|
558
|
+
const sourceCompletionAt = candidate.events
|
|
559
|
+
.map((item) => Date.parse(item.occurredAt))
|
|
560
|
+
.filter(Number.isFinite)
|
|
561
|
+
.sort((a, b) => b - a)[0] ?? evaluationNow.getTime();
|
|
562
|
+
const deadlineConditionId = `pattern-v2:${signature}`;
|
|
563
|
+
const deadlineCompletionAt = reachedDeadlines
|
|
564
|
+
.filter((deadline) => deadline.conditionId === deadlineConditionId)
|
|
565
|
+
.map((deadline) => Date.parse(deadline.dueAt))
|
|
566
|
+
.filter(Number.isFinite)
|
|
567
|
+
.sort((a, b) => b - a)[0];
|
|
568
|
+
const completedAt = new Date(deadlineCompletionAt ?? sourceCompletionAt).toISOString();
|
|
539
569
|
const lastSemantic = semanticDecisions.at(-1)?.decision ?? null;
|
|
540
570
|
const matched = TriggerMatchRecordSchema.parse({
|
|
541
571
|
protocolVersion: COMPOSITE_TRIGGER_PROTOCOL_VERSION,
|
|
@@ -544,16 +574,17 @@ export class CompositeTriggerEngine {
|
|
|
544
574
|
triggerId: definition.triggerId,
|
|
545
575
|
triggerVersion: definition.version,
|
|
546
576
|
status: 'matched',
|
|
547
|
-
|
|
577
|
+
partitionKey: buffer.partitionKey,
|
|
548
578
|
openedAt,
|
|
549
579
|
expiresAt: new Date(Date.parse(openedAt) + definition.withinMs).toISOString(),
|
|
550
580
|
updatedAt: nowIso,
|
|
551
581
|
sourceEvents: candidate.events,
|
|
552
|
-
|
|
582
|
+
semanticDecision: lastSemantic,
|
|
553
583
|
patternState: {
|
|
554
584
|
version: '2',
|
|
555
585
|
role: 'match',
|
|
556
586
|
signature,
|
|
587
|
+
completedAt,
|
|
557
588
|
semanticDecisions,
|
|
558
589
|
},
|
|
559
590
|
firedWakeId: null,
|
|
@@ -638,301 +669,8 @@ export class CompositeTriggerEngine {
|
|
|
638
669
|
}
|
|
639
670
|
return results;
|
|
640
671
|
}
|
|
641
|
-
async
|
|
642
|
-
|
|
643
|
-
const eventTime = Date.parse(event.occurredAt);
|
|
644
|
-
const key = deterministicKeyForClause(definition, clause.id, event);
|
|
645
|
-
const relatedMatches = this.store
|
|
646
|
-
.listTriggerMatches(definition.triggerId)
|
|
647
|
-
.filter((record) => record.triggerVersion === definition.version &&
|
|
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')
|
|
652
|
-
.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0];
|
|
653
|
-
if (alreadyMatched) {
|
|
654
|
-
return {
|
|
655
|
-
triggerId: definition.triggerId,
|
|
656
|
-
match: alreadyMatched,
|
|
657
|
-
matched: true,
|
|
658
|
-
fired: false,
|
|
659
|
-
};
|
|
660
|
-
}
|
|
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);
|
|
675
|
-
const expired = TriggerMatchRecordSchema.parse({
|
|
676
|
-
...candidate,
|
|
677
|
-
status: 'expired',
|
|
678
|
-
updatedAt: nowIso,
|
|
679
|
-
});
|
|
680
|
-
await this.store.appendTriggerMatch(expired);
|
|
681
|
-
await this.support.cancelDeadlinesForMatch(expired.matchId);
|
|
682
|
-
await this.support.auditMatch(expired, 'trigger.expired', {
|
|
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
|
-
},
|
|
729
|
-
});
|
|
730
|
-
return {
|
|
731
|
-
triggerId: definition.triggerId,
|
|
732
|
-
match: null,
|
|
733
|
-
matched: false,
|
|
734
|
-
fired: false,
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
record ??= await this.newMatch(definition, key, event);
|
|
738
|
-
const duplicate = record.sourceEvents.some((candidate) => sameEventIdentity(candidate, event));
|
|
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);
|
|
747
|
-
record = TriggerMatchRecordSchema.parse({
|
|
748
|
-
...record,
|
|
749
|
-
correlationKey: record.correlationKey ?? key,
|
|
750
|
-
openedAt: selectedWindow.openedAt,
|
|
751
|
-
expiresAt: selectedWindow.expiresAt,
|
|
752
|
-
updatedAt: nowIso,
|
|
753
|
-
sourceEvents: [
|
|
754
|
-
...record.sourceEvents,
|
|
755
|
-
asSourceEvent(clause.id, event),
|
|
756
|
-
],
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
if (!expressionSatisfied(definition, record)) {
|
|
760
|
-
await this.store.appendTriggerMatch(record);
|
|
761
|
-
await this.support.auditMatch(record, 'trigger.partial', {
|
|
762
|
-
clauseId: clause.id,
|
|
763
|
-
sourceEventId: event.sourceEventId,
|
|
764
|
-
occurredAt: event.occurredAt,
|
|
765
|
-
receivedAt: event.receivedAt ?? null,
|
|
766
|
-
eventTimeWindow: {
|
|
767
|
-
openedAt: record.openedAt,
|
|
768
|
-
expiresAt: record.expiresAt,
|
|
769
|
-
},
|
|
770
|
-
});
|
|
771
|
-
return {
|
|
772
|
-
triggerId: definition.triggerId,
|
|
773
|
-
match: record,
|
|
774
|
-
matched: false,
|
|
775
|
-
fired: false,
|
|
776
|
-
};
|
|
777
|
-
}
|
|
778
|
-
return this.finalizeEligible(definition, record, this.now());
|
|
779
|
-
}
|
|
780
|
-
async finalizeEligible(definition, record, evaluationNow) {
|
|
781
|
-
const nowIso = evaluationNow.toISOString();
|
|
782
|
-
const temporal = evaluateTemporalConditions(definition, record, evaluationNow);
|
|
783
|
-
if (temporal.outcome === 'blocked') {
|
|
784
|
-
const expired = TriggerMatchRecordSchema.parse({
|
|
785
|
-
...record,
|
|
786
|
-
status: 'expired',
|
|
787
|
-
updatedAt: nowIso,
|
|
788
|
-
});
|
|
789
|
-
await this.store.appendTriggerMatch(expired);
|
|
790
|
-
await this.support.cancelDeadlinesForMatch(expired.matchId);
|
|
791
|
-
await this.support.auditMatch(expired, 'trigger.expired', {
|
|
792
|
-
reason: 'temporal_condition_blocked',
|
|
793
|
-
temporal: temporal.conditionStates,
|
|
794
|
-
});
|
|
795
|
-
return {
|
|
796
|
-
triggerId: definition.triggerId,
|
|
797
|
-
match: expired,
|
|
798
|
-
matched: false,
|
|
799
|
-
fired: false,
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
if (temporal.outcome === 'pending') {
|
|
803
|
-
await this.store.appendTriggerMatch(TriggerMatchRecordSchema.parse({
|
|
804
|
-
...record,
|
|
805
|
-
status: 'partial',
|
|
806
|
-
updatedAt: nowIso,
|
|
807
|
-
}));
|
|
808
|
-
await this.support.scheduleDeadlines(definition, record, temporal.pendingDeadlines);
|
|
809
|
-
await this.support.auditMatch(record, 'trigger.partial', {
|
|
810
|
-
reason: 'temporal_wait',
|
|
811
|
-
temporal: temporal.conditionStates,
|
|
812
|
-
});
|
|
813
|
-
return {
|
|
814
|
-
triggerId: definition.triggerId,
|
|
815
|
-
match: record,
|
|
816
|
-
matched: false,
|
|
817
|
-
fired: false,
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
|
-
await this.support.cancelDeadlinesForMatch(record.matchId);
|
|
821
|
-
if (definition.correlation?.semantic) {
|
|
822
|
-
if (!this.evaluator) {
|
|
823
|
-
await this.store.appendTriggerMatch(record);
|
|
824
|
-
await this.support.auditMatch(record, 'trigger.partial', {
|
|
825
|
-
reason: 'semantic_evaluator_unavailable',
|
|
826
|
-
});
|
|
827
|
-
return {
|
|
828
|
-
triggerId: definition.triggerId,
|
|
829
|
-
match: record,
|
|
830
|
-
matched: false,
|
|
831
|
-
fired: false,
|
|
832
|
-
};
|
|
833
|
-
}
|
|
834
|
-
const semantic = definition.correlation.semantic;
|
|
835
|
-
const decision = await new SemanticConditionEngine(this.evaluator).evaluate(semanticSource(record), {
|
|
836
|
-
type: 'semantic_boolean',
|
|
837
|
-
instruction: semantic.instruction,
|
|
838
|
-
input: semantic.input,
|
|
839
|
-
matchThreshold: semantic.matchThreshold,
|
|
840
|
-
rejectThreshold: semantic.rejectThreshold,
|
|
841
|
-
uncertain: semantic.uncertain,
|
|
842
|
-
});
|
|
843
|
-
record = TriggerMatchRecordSchema.parse({
|
|
844
|
-
...record,
|
|
845
|
-
correlationDecision: {
|
|
846
|
-
evaluator: decision.evaluator,
|
|
847
|
-
outcome: decision.outcome,
|
|
848
|
-
probability: decision.probability,
|
|
849
|
-
matched: decision.matched,
|
|
850
|
-
shouldEscalate: decision.shouldEscalate,
|
|
851
|
-
inputFields: semantic.input,
|
|
852
|
-
...(decision.metadata ? { providerEvidence: decision.metadata } : {}),
|
|
853
|
-
evaluatedAt: nowIso,
|
|
854
|
-
},
|
|
855
|
-
updatedAt: nowIso,
|
|
856
|
-
});
|
|
857
|
-
await this.store.appendTriggerMatch(record);
|
|
858
|
-
await this.support.auditMatch(record, 'trigger.correlation', {
|
|
859
|
-
outcome: decision.outcome,
|
|
860
|
-
probability: decision.probability,
|
|
861
|
-
evaluator: decision.evaluator,
|
|
862
|
-
});
|
|
863
|
-
if (!decision.matched) {
|
|
864
|
-
return {
|
|
865
|
-
triggerId: definition.triggerId,
|
|
866
|
-
match: record,
|
|
867
|
-
matched: false,
|
|
868
|
-
fired: false,
|
|
869
|
-
};
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
const matched = TriggerMatchRecordSchema.parse({
|
|
873
|
-
...record,
|
|
874
|
-
status: 'matched',
|
|
875
|
-
updatedAt: nowIso,
|
|
876
|
-
});
|
|
877
|
-
await this.store.appendTriggerMatch(matched);
|
|
878
|
-
await this.support.auditMatch(matched, 'trigger.matched', {
|
|
879
|
-
sourceEventIds: matched.sourceEvents.map((item) => item.sourceEventId),
|
|
880
|
-
temporal: temporal.conditionStates,
|
|
881
|
-
});
|
|
882
|
-
return {
|
|
883
|
-
triggerId: definition.triggerId,
|
|
884
|
-
match: matched,
|
|
885
|
-
matched: true,
|
|
886
|
-
fired: false,
|
|
887
|
-
};
|
|
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
|
-
}
|
|
907
|
-
async newMatch(definition, key, event) {
|
|
908
|
-
const openedAt = event.occurredAt;
|
|
909
|
-
const expiresAt = new Date(Date.parse(openedAt) + definition.withinMs).toISOString();
|
|
910
|
-
const matchId = `tm_${(await sha256Hex([
|
|
911
|
-
definition.triggerId,
|
|
912
|
-
definition.version,
|
|
913
|
-
key ?? '-',
|
|
914
|
-
event.sourceEventId,
|
|
915
|
-
].join(':'))).slice(0, 24)}`;
|
|
916
|
-
return TriggerMatchRecordSchema.parse({
|
|
917
|
-
protocolVersion: COMPOSITE_TRIGGER_PROTOCOL_VERSION,
|
|
918
|
-
schemaVersion: COMPOSITE_TRIGGER_SCHEMA_VERSION,
|
|
919
|
-
matchId,
|
|
920
|
-
triggerId: definition.triggerId,
|
|
921
|
-
triggerVersion: definition.version,
|
|
922
|
-
status: 'partial',
|
|
923
|
-
correlationKey: key,
|
|
924
|
-
openedAt,
|
|
925
|
-
expiresAt,
|
|
926
|
-
updatedAt: this.now().toISOString(),
|
|
927
|
-
sourceEvents: [],
|
|
928
|
-
correlationDecision: null,
|
|
929
|
-
firedWakeId: null,
|
|
930
|
-
derivedEventIds: [],
|
|
931
|
-
});
|
|
932
|
-
}
|
|
933
|
-
latestMatch(matchId) {
|
|
934
|
-
return this.store
|
|
935
|
-
.listTriggerMatches()
|
|
672
|
+
async latestMatch(matchId) {
|
|
673
|
+
return (await this.store.listTriggerMatches())
|
|
936
674
|
.filter((record) => record.matchId === matchId)
|
|
937
675
|
.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0] ?? null;
|
|
938
676
|
}
|