mcp-event-intelligence 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +31 -38
  3. package/SECURITY.md +4 -4
  4. package/bin/mcp-event-intelligence.mjs +2 -2
  5. package/dist/src/composite/engine.d.ts +11 -7
  6. package/dist/src/composite/engine.js +150 -414
  7. package/dist/src/composite/matching.d.ts +1 -5
  8. package/dist/src/composite/matching.js +0 -54
  9. package/dist/src/composite/runtimeSupport.js +10 -6
  10. package/dist/src/composite/store.d.ts +38 -4
  11. package/dist/src/composite/temporal.d.ts +0 -17
  12. package/dist/src/composite/temporal.js +0 -266
  13. package/dist/src/intelligenceProtocol/activationSchemas.d.ts +36 -3
  14. package/dist/src/intelligenceProtocol/activationSchemas.js +4 -3
  15. package/dist/src/intelligenceProtocol/index.d.ts +8 -12
  16. package/dist/src/intelligenceProtocol/index.js +4 -7
  17. package/dist/src/intelligenceProtocol/lifecycle.js +2 -10
  18. package/dist/src/intelligenceProtocol/operatorRegistry.d.ts +3 -270
  19. package/dist/src/intelligenceProtocol/operatorRegistry.js +5 -144
  20. package/dist/src/intelligenceProtocol/schemas.d.ts +0 -189
  21. package/dist/src/intelligenceProtocol/schemas.js +1 -97
  22. package/dist/src/intelligenceProtocol/triggerPlanSchemas.d.ts +5 -102
  23. package/dist/src/intelligenceProtocol/triggerPlanSchemas.js +2 -26
  24. package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +14 -215
  25. package/dist/src/intelligenceProtocol/triggerSchemas.js +18 -191
  26. package/dist/src/intelligenceProtocol/version.d.ts +2 -2
  27. package/dist/src/intelligenceProtocol/version.js +2 -2
  28. package/dist/src/patternV2/evaluator.d.ts +9 -1
  29. package/dist/src/patternV2/evaluator.js +63 -4
  30. package/dist/src/semantic/types.d.ts +6 -0
  31. package/dist/src/semantic/typesafeJevEvaluator.d.ts +1 -0
  32. package/dist/src/semantic/typesafeJevEvaluator.js +6 -0
  33. package/package.json +5 -2
  34. package/scripts/host-integration.d.mts +48 -5
  35. package/scripts/host-integration.mjs +16 -4
  36. package/scripts/lib/activation-hydrator.mjs +12 -12
  37. package/scripts/lib/composite-event-consumer.mjs +8 -28
  38. package/scripts/lib/composite-wake-coordinator.mjs +19 -19
  39. package/scripts/lib/derived-contract-registry.mjs +4 -4
  40. package/scripts/lib/derived-event-coordinator.mjs +11 -22
  41. package/scripts/lib/local-event-intelligence-runtime.mjs +35 -3
  42. package/scripts/lib/mcp-events-client.mjs +36 -32
  43. package/scripts/lib/persistent-event-store.mjs +228 -61
  44. package/scripts/lib/store-capabilities.mjs +88 -0
  45. package/scripts/lib/store-conformance.mjs +127 -0
  46. package/scripts/lib/temporal-deadline-scheduler.mjs +3 -3
  47. package/scripts/lib/trigger-control-plane.mjs +52 -57
  48. package/scripts/lib/trigger-inspector.mjs +48 -160
  49. package/scripts/lib/trigger-planner.mjs +37 -126
  50. package/scripts/lib/wake-retry-scheduler.mjs +2 -19
  51. package/scripts/mcp-stdio-server.mjs +8 -8
  52. package/scripts/service.mjs +19 -138
  53. package/server.json +3 -3
  54. package/dist/src/intelligenceProtocol/invariants.d.ts +0 -3
  55. package/dist/src/intelligenceProtocol/invariants.js +0 -36
  56. package/dist/src/intelligenceProtocol/mapper.d.ts +0 -14
  57. package/dist/src/intelligenceProtocol/mapper.js +0 -28
  58. package/dist/src/patternV2/legacyCompiler.d.ts +0 -3
  59. package/dist/src/patternV2/legacyCompiler.js +0 -184
  60. package/scripts/lib/event-processor.mjs +0 -624
  61. 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 { evaluateTemporalConditions, } from './temporal.js';
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
- constructor(store, evaluator = null, now = () => new Date()) {
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
- assertNoDerivedEventCycle(this.store.listTriggers(), definition, this.store.getTriggerState?.bind(this.store));
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?.(definition.triggerId, definition.version) ?? null;
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
- 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
- }
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
- const clause = matchingClauses[0];
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?.(definition.triggerId, definition.version) ?? null;
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 (definition.patternV2 &&
227
- match.patternState?.version === '2' &&
193
+ if (match.patternState?.version === '2' &&
228
194
  match.patternState.role === 'buffer') {
229
- return this.evaluatePatternV2Buffer(definition, match, new Date(event.occurredAt));
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
- const result = await this.finalizeEligible(definition, match, new Date(event.occurredAt));
250
- return [result];
197
+ return [];
251
198
  }
252
199
  patternPartitionKey(definition, matchingClauses, event) {
253
- const dimensions = definition.patternV2?.partitionBy ?? [];
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 applyPatternV2(definition, matchingClauses, event) {
309
- const pattern = definition.patternV2;
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
- correlationKey: key,
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
- correlationKey: key,
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
- correlationDecision: null,
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.evaluatePatternV2Buffer(definition, buffer, now);
397
+ return this.evaluatePatternBuffer(definition, buffer, now);
411
398
  }
412
- async evaluatePatternV2Buffer(definition, buffer, evaluationNow) {
413
- const pattern = definition.patternV2;
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?.(definition.triggerId, definition.version) ?? null;
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,21 @@ export class CompositeTriggerEngine {
530
549
  const matchId = `tm_${(await sha256Hex([
531
550
  definition.triggerId,
532
551
  definition.version,
533
- buffer.correlationKey ?? '-',
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 deadlineCompletionAt = reachedDeadlines
563
+ .map((deadline) => Date.parse(deadline.dueAt))
564
+ .filter(Number.isFinite)
565
+ .sort((a, b) => b - a)[0];
566
+ const completedAt = new Date(deadlineCompletionAt ?? sourceCompletionAt).toISOString();
539
567
  const lastSemantic = semanticDecisions.at(-1)?.decision ?? null;
540
568
  const matched = TriggerMatchRecordSchema.parse({
541
569
  protocolVersion: COMPOSITE_TRIGGER_PROTOCOL_VERSION,
@@ -544,16 +572,17 @@ export class CompositeTriggerEngine {
544
572
  triggerId: definition.triggerId,
545
573
  triggerVersion: definition.version,
546
574
  status: 'matched',
547
- correlationKey: buffer.correlationKey,
575
+ partitionKey: buffer.partitionKey,
548
576
  openedAt,
549
577
  expiresAt: new Date(Date.parse(openedAt) + definition.withinMs).toISOString(),
550
578
  updatedAt: nowIso,
551
579
  sourceEvents: candidate.events,
552
- correlationDecision: lastSemantic,
580
+ semanticDecision: lastSemantic,
553
581
  patternState: {
554
582
  version: '2',
555
583
  role: 'match',
556
584
  signature,
585
+ completedAt,
557
586
  semanticDecisions,
558
587
  },
559
588
  firedWakeId: null,
@@ -638,301 +667,8 @@ export class CompositeTriggerEngine {
638
667
  }
639
668
  return results;
640
669
  }
641
- async applyEvent(definition, clause, event) {
642
- const nowIso = this.now().toISOString();
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()
670
+ async latestMatch(matchId) {
671
+ return (await this.store.listTriggerMatches())
936
672
  .filter((record) => record.matchId === matchId)
937
673
  .sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0] ?? null;
938
674
  }