mcp-event-intelligence 0.4.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 (64) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +107 -26
  3. package/SECURITY.md +4 -4
  4. package/bin/mcp-event-intelligence.mjs +3 -3
  5. package/dist/src/composite/engine.d.ts +14 -4
  6. package/dist/src/composite/engine.js +533 -208
  7. package/dist/src/composite/matching.d.ts +1 -5
  8. package/dist/src/composite/matching.js +45 -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 +38 -3
  14. package/dist/src/intelligenceProtocol/activationSchemas.js +5 -3
  15. package/dist/src/intelligenceProtocol/index.d.ts +12 -9
  16. package/dist/src/intelligenceProtocol/index.js +6 -5
  17. package/dist/src/intelligenceProtocol/lifecycle.js +2 -10
  18. package/dist/src/intelligenceProtocol/operatorRegistry.d.ts +300 -0
  19. package/dist/src/intelligenceProtocol/operatorRegistry.js +338 -0
  20. package/dist/src/intelligenceProtocol/patternV2Schemas.d.ts +188 -0
  21. package/dist/src/intelligenceProtocol/patternV2Schemas.js +801 -0
  22. package/dist/src/intelligenceProtocol/schemas.d.ts +4 -189
  23. package/dist/src/intelligenceProtocol/schemas.js +3 -97
  24. package/dist/src/intelligenceProtocol/triggerPlanSchemas.d.ts +114 -38
  25. package/dist/src/intelligenceProtocol/triggerPlanSchemas.js +27 -18
  26. package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +268 -213
  27. package/dist/src/intelligenceProtocol/triggerSchemas.js +155 -169
  28. package/dist/src/intelligenceProtocol/version.d.ts +2 -2
  29. package/dist/src/intelligenceProtocol/version.js +2 -2
  30. package/dist/src/mcpEvents/consumer.d.ts +1 -0
  31. package/dist/src/mcpEvents/consumer.js +1 -0
  32. package/dist/src/patternV2/evaluator.d.ts +51 -0
  33. package/dist/src/patternV2/evaluator.js +986 -0
  34. package/dist/src/semantic/types.d.ts +6 -0
  35. package/dist/src/semantic/typesafeJevEvaluator.d.ts +1 -0
  36. package/dist/src/semantic/typesafeJevEvaluator.js +6 -0
  37. package/package.json +9 -3
  38. package/scripts/host-integration.d.mts +54 -4
  39. package/scripts/host-integration.mjs +25 -4
  40. package/scripts/lib/activation-hydrator.mjs +13 -12
  41. package/scripts/lib/composite-event-consumer.mjs +10 -28
  42. package/scripts/lib/composite-wake-coordinator.mjs +19 -19
  43. package/scripts/lib/derived-contract-registry.mjs +4 -4
  44. package/scripts/lib/derived-event-coordinator.mjs +69 -6
  45. package/scripts/lib/local-event-intelligence-runtime.mjs +35 -3
  46. package/scripts/lib/mcp-events-client.mjs +97 -55
  47. package/scripts/lib/mcp-events-compatibility.mjs +54 -0
  48. package/scripts/lib/persistent-event-store.mjs +228 -61
  49. package/scripts/lib/store-capabilities.mjs +88 -0
  50. package/scripts/lib/store-conformance.mjs +127 -0
  51. package/scripts/lib/temporal-deadline-scheduler.mjs +3 -3
  52. package/scripts/lib/trigger-control-plane.mjs +202 -53
  53. package/scripts/lib/trigger-inspector.mjs +50 -147
  54. package/scripts/lib/trigger-planner.mjs +141 -52
  55. package/scripts/lib/wake-retry-scheduler.mjs +2 -19
  56. package/scripts/mcp-stdio-server.mjs +46 -61
  57. package/scripts/service.mjs +19 -138
  58. package/server.json +4 -4
  59. package/dist/src/intelligenceProtocol/invariants.d.ts +0 -3
  60. package/dist/src/intelligenceProtocol/invariants.js +0 -36
  61. package/dist/src/intelligenceProtocol/mapper.d.ts +0 -14
  62. package/dist/src/intelligenceProtocol/mapper.js +0 -28
  63. package/scripts/lib/event-processor.mjs +0 -624
  64. package/scripts/lib/http-wake-deliverer.mjs +0 -31
@@ -1,24 +1,71 @@
1
- import { SemanticConditionEngine } from '../semantic/conditionEngine.js';
1
+ import { evaluatePatternV2, patternV2CandidateSignature, } from '../patternV2/evaluator.js';
2
2
  import { sha256Hex } from '../intelligenceProtocol/canonical.js';
3
3
  import { COMPOSITE_TRIGGER_PROTOCOL_VERSION, COMPOSITE_TRIGGER_SCHEMA_VERSION, CompositeTriggerDefinitionSchema, CorrelatableEventSchema, TriggerMatchRecordSchema, } from '../intelligenceProtocol/triggerSchemas.js';
4
- import { evaluateTemporalConditions, } from './temporal.js';
5
- import { asSourceEvent, clauseMatches, deterministicKeyForClause, expressionSatisfied, matchesCorrelationKey, sameEventIdentity, semanticSource, } from './matching.js';
4
+ import { asSourceEvent, clauseMatches, sameEventIdentity, } from './matching.js';
6
5
  import { assertNoDerivedEventCycle } from './derivedGraph.js';
7
6
  import { CompositeTriggerRuntimeSupport, } from './runtimeSupport.js';
7
+ function getByPath(source, path) {
8
+ let current = source;
9
+ for (const part of path.split('.')) {
10
+ if (current === null ||
11
+ typeof current !== 'object' ||
12
+ Array.isArray(current))
13
+ return undefined;
14
+ current = current[part];
15
+ }
16
+ return current;
17
+ }
18
+ function sourceEventOrder(left, right) {
19
+ return (Date.parse(left.occurredAt) - Date.parse(right.occurredAt) ||
20
+ left.sourceEventId.localeCompare(right.sourceEventId) ||
21
+ left.clauseId.localeCompare(right.clauseId));
22
+ }
23
+ function stablePatternValue(value) {
24
+ if (Array.isArray(value))
25
+ return value.map(stablePatternValue);
26
+ if (value && typeof value === 'object') {
27
+ return Object.fromEntries(Object.keys(value)
28
+ .sort()
29
+ .map((key) => [
30
+ key,
31
+ stablePatternValue(value[key]),
32
+ ]));
33
+ }
34
+ return value;
35
+ }
36
+ function patternPhysicalIdentity(event) {
37
+ return [
38
+ event.serverId ?? '-',
39
+ event.sourceEventId,
40
+ JSON.stringify(stablePatternValue(event.subscriptionArguments ?? {})),
41
+ ].join(':');
42
+ }
8
43
  export class CompositeTriggerEngine {
9
44
  store;
10
45
  evaluator;
11
46
  now;
12
47
  support;
13
- constructor(store, evaluator = null, now = () => new Date()) {
48
+ workerId;
49
+ partitionLeaseMs;
50
+ constructor(store, evaluator = null, now = () => new Date(), options = {}) {
14
51
  this.store = store;
15
52
  this.evaluator = evaluator;
16
53
  this.now = now;
17
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);
18
58
  }
19
59
  async register(definitionInput) {
20
60
  const definition = CompositeTriggerDefinitionSchema.parse(definitionInput);
21
- 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);
22
69
  await this.store.putTrigger(definition);
23
70
  return definition;
24
71
  }
@@ -28,17 +75,33 @@ export class CompositeTriggerEngine {
28
75
  return this.ingestTimerEvent(event);
29
76
  }
30
77
  const results = [];
31
- for (const definition of this.store.listTriggers()) {
32
- 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;
33
82
  if (!(await this.support.lifecycleAllows(definition, triggerState, this.now()))) {
34
83
  continue;
35
84
  }
36
85
  const matchingClauses = definition.clauses.filter((clause) => clauseMatches(clause, event));
37
86
  if (matchingClauses.length === 0)
38
87
  continue;
39
- const replayed = this.store
40
- .listTriggerMatches(definition.triggerId)
41
- .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];
42
105
  if (replayed) {
43
106
  results.push({
44
107
  triggerId: definition.triggerId,
@@ -50,13 +113,12 @@ export class CompositeTriggerEngine {
50
113
  });
51
114
  continue;
52
115
  }
53
- const clause = matchingClauses[0];
54
- results.push(await this.applyEvent(definition, clause, event));
116
+ results.push(...await this.applyPattern(definition, matchingClauses, event));
55
117
  }
56
118
  return results;
57
119
  }
58
120
  async markFired(matchId, wakeId) {
59
- const latest = this.latestMatch(matchId);
121
+ const latest = await this.latestMatch(matchId);
60
122
  if (!latest)
61
123
  throw new Error(`Unknown trigger match: ${matchId}`);
62
124
  if (latest.status === 'fired')
@@ -77,7 +139,7 @@ export class CompositeTriggerEngine {
77
139
  return fired;
78
140
  }
79
141
  async markDerivedEmitted(matchId, derivedEventId) {
80
- const latest = this.latestMatch(matchId);
142
+ const latest = await this.latestMatch(matchId);
81
143
  if (!latest)
82
144
  throw new Error(`Unknown trigger match: ${matchId}`);
83
145
  if (latest.status === 'emitted' &&
@@ -107,18 +169,20 @@ export class CompositeTriggerEngine {
107
169
  const deadlineId = String(event.data.deadlineId ?? '');
108
170
  if (!deadlineId || !this.store.getTemporalDeadline)
109
171
  return [];
110
- const deadline = this.store.getTemporalDeadline(deadlineId);
172
+ const deadline = await this.store.getTemporalDeadline(deadlineId);
111
173
  if (!deadline || deadline.status !== 'pending')
112
174
  return [];
113
- const definition = this.store.listTriggers().find((candidate) => candidate.triggerId === deadline.triggerId &&
175
+ const definition = (await this.store.listTriggers()).find((candidate) => candidate.triggerId === deadline.triggerId &&
114
176
  candidate.version === deadline.triggerVersion);
115
177
  if (!definition)
116
178
  return [];
117
- 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;
118
182
  if (!(await this.support.lifecycleAllows(definition, triggerState, this.now()))) {
119
183
  return [];
120
184
  }
121
- const match = this.latestMatch(deadline.matchId);
185
+ const match = await this.latestMatch(deadline.matchId);
122
186
  if (!match ||
123
187
  match.status === 'matched' ||
124
188
  match.status === 'emitted' ||
@@ -126,224 +190,485 @@ export class CompositeTriggerEngine {
126
190
  match.status === 'expired') {
127
191
  return [];
128
192
  }
129
- if (Date.parse(event.occurredAt) > Date.parse(match.expiresAt)) {
130
- const expired = TriggerMatchRecordSchema.parse({
131
- ...match,
132
- status: 'expired',
133
- updatedAt: event.occurredAt,
134
- });
135
- await this.store.appendTriggerMatch(expired);
136
- await this.support.cancelDeadlinesForMatch(expired.matchId);
137
- await this.support.auditMatch(expired, 'trigger.expired', {
138
- reason: 'temporal_deadline_after_event_window',
193
+ if (match.patternState?.version === '2' &&
194
+ match.patternState.role === 'buffer') {
195
+ return this.evaluatePatternBuffer(definition, match, new Date(event.occurredAt));
196
+ }
197
+ return [];
198
+ }
199
+ patternPartitionKey(definition, matchingClauses, event) {
200
+ const dimensions = definition.pattern?.partitionBy ?? [];
201
+ if (!dimensions.length)
202
+ return null;
203
+ const clauseIds = new Set(matchingClauses.map((clause) => clause.id));
204
+ const components = [];
205
+ for (const dimension of dimensions) {
206
+ const fields = dimension.fields.filter((field) => clauseIds.has(field.ref));
207
+ if (!fields.length) {
208
+ throw new Error(`Pattern partition ${dimension.key} has no field for incoming event ${event.name}`);
209
+ }
210
+ const values = fields.map((field) => getByPath(event.data, field.path));
211
+ const first = values[0];
212
+ if (first === undefined ||
213
+ first === null ||
214
+ !['string', 'number', 'boolean'].includes(typeof first)) {
215
+ throw new Error(`Pattern partition ${dimension.key} resolved to a non-scalar value`);
216
+ }
217
+ if (values.some((value) => typeof value !== typeof first ||
218
+ value !== first)) {
219
+ throw new Error(`Pattern partition ${dimension.key} is ambiguous for incoming event ${event.name}`);
220
+ }
221
+ components.push({
222
+ key: dimension.key,
223
+ type: typeof first,
224
+ value: first,
139
225
  });
140
- return [{
141
- triggerId: definition.triggerId,
142
- match: expired,
143
- matched: false,
144
- fired: false,
145
- }];
146
226
  }
147
- const result = await this.finalizeEligible(definition, match, new Date(event.occurredAt));
148
- return [result];
227
+ return JSON.stringify(components);
149
228
  }
150
- async applyEvent(definition, clause, event) {
151
- const nowIso = this.now().toISOString();
152
- const eventTime = Date.parse(event.occurredAt);
153
- const key = deterministicKeyForClause(definition, clause.id, event);
154
- const existing = this.store
155
- .listTriggerMatches(definition.triggerId)
156
- .filter((record) => record.triggerVersion === definition.version &&
157
- (record.status === 'partial' || record.status === 'matched') &&
158
- matchesCorrelationKey(record, key))
159
- .sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0];
160
- if (existing?.status === 'matched') {
161
- return {
162
- triggerId: definition.triggerId,
163
- match: existing,
164
- matched: true,
165
- fired: false,
166
- };
229
+ async patternBufferId(definition, key) {
230
+ return `tm_${(await sha256Hex([
231
+ definition.triggerId,
232
+ definition.version,
233
+ key ?? '-',
234
+ 'pattern-v2-buffer',
235
+ ].join(':'))).slice(0, 24)}`;
236
+ }
237
+ patternDecisionRecord(item, evaluatedAt) {
238
+ const decision = item.decision;
239
+ return {
240
+ nodeId: item.nodeId,
241
+ decision: {
242
+ evaluator: decision.evaluator,
243
+ outcome: decision.outcome,
244
+ probability: decision.probability,
245
+ matched: decision.matched,
246
+ shouldEscalate: decision.shouldEscalate,
247
+ inputFields: decision.condition.input,
248
+ ...(decision.metadata
249
+ ? { providerEvidence: decision.metadata }
250
+ : {}),
251
+ evaluatedAt,
252
+ },
253
+ };
254
+ }
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);
167
261
  }
168
- let record = existing ?? await this.newMatch(definition, key, event);
169
- if (Date.parse(record.expiresAt) < eventTime) {
170
- const expired = TriggerMatchRecordSchema.parse({
171
- ...record,
172
- status: 'expired',
173
- updatedAt: nowIso,
174
- });
175
- await this.store.appendTriggerMatch(expired);
176
- await this.support.cancelDeadlinesForMatch(expired.matchId);
177
- await this.support.auditMatch(expired, 'trigger.expired', {
178
- reason: 'window_expired_before_event',
179
- });
180
- record = await this.newMatch(definition, key, event);
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;
181
268
  }
182
- const duplicate = record.sourceEvents.some((candidate) => sameEventIdentity(candidate, event));
183
- if (!duplicate) {
184
- record = TriggerMatchRecordSchema.parse({
185
- ...record,
186
- correlationKey: record.correlationKey ?? key,
187
- updatedAt: nowIso,
188
- sourceEvents: [
189
- ...record.sourceEvents,
190
- asSourceEvent(clause.id, event),
191
- ],
192
- });
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);
193
288
  }
194
- if (!expressionSatisfied(definition, record)) {
195
- await this.store.appendTriggerMatch(record);
196
- await this.support.auditMatch(record, 'trigger.partial', {
197
- clauseId: clause.id,
198
- sourceEventId: event.sourceEventId,
289
+ finally {
290
+ await this.store.releasePartitionLease(leaseKey, {
291
+ workerId: this.workerId,
292
+ now: this.now().toISOString(),
199
293
  });
200
- return {
201
- triggerId: definition.triggerId,
202
- match: record,
203
- matched: false,
204
- fired: false,
205
- };
206
294
  }
207
- return this.finalizeEligible(definition, record, this.now());
208
295
  }
209
- async finalizeEligible(definition, record, evaluationNow) {
210
- const nowIso = evaluationNow.toISOString();
211
- const temporal = evaluateTemporalConditions(definition, record, evaluationNow);
212
- if (temporal.outcome === 'blocked') {
213
- const expired = TriggerMatchRecordSchema.parse({
214
- ...record,
215
- status: 'expired',
216
- updatedAt: nowIso,
217
- });
218
- await this.store.appendTriggerMatch(expired);
219
- await this.support.cancelDeadlinesForMatch(expired.matchId);
220
- await this.support.auditMatch(expired, 'trigger.expired', {
221
- reason: 'temporal_condition_blocked',
222
- temporal: temporal.conditionStates,
223
- });
224
- return {
225
- triggerId: definition.triggerId,
226
- match: expired,
227
- matched: false,
228
- fired: false,
229
- };
296
+ async applyPatternWithPartition(definition, matchingClauses, event, key) {
297
+ const pattern = definition.pattern;
298
+ if (!pattern)
299
+ return [];
300
+ const now = this.now();
301
+ const nowIso = now.toISOString();
302
+ const bufferId = await this.patternBufferId(definition, key);
303
+ const existing = await this.latestMatch(bufferId);
304
+ let sourceEvents = existing?.sourceEvents
305
+ ? [...existing.sourceEvents]
306
+ : [];
307
+ for (const clause of matchingClauses) {
308
+ const duplicate = sourceEvents.some((source) => source.clauseId === clause.id &&
309
+ sameEventIdentity(source, event));
310
+ if (!duplicate) {
311
+ sourceEvents.push(asSourceEvent(clause.id, event));
312
+ }
230
313
  }
231
- if (temporal.outcome === 'pending') {
232
- await this.store.appendTriggerMatch(TriggerMatchRecordSchema.parse({
233
- ...record,
234
- status: 'partial',
235
- updatedAt: nowIso,
236
- }));
237
- await this.support.scheduleDeadlines(definition, record, temporal.pendingDeadlines);
238
- await this.support.auditMatch(record, 'trigger.partial', {
239
- reason: 'temporal_wait',
240
- temporal: temporal.conditionStates,
314
+ sourceEvents.sort(sourceEventOrder);
315
+ const previousMaxObserved = existing?.patternState
316
+ ?.maxObservedOccurredAt
317
+ ? Date.parse(existing.patternState.maxObservedOccurredAt)
318
+ : Number.NEGATIVE_INFINITY;
319
+ const maxObserved = Math.max(previousMaxObserved, Date.parse(event.occurredAt), ...sourceEvents
320
+ .map((source) => Date.parse(source.occurredAt))
321
+ .filter(Number.isFinite));
322
+ const allowedLatenessMs = definition.eventTime?.allowedLatenessMs ?? 0;
323
+ const watermarkMs = maxObserved - allowedLatenessMs;
324
+ const retentionFloor = watermarkMs - definition.withinMs;
325
+ sourceEvents = sourceEvents.filter((source) => Date.parse(source.occurredAt) >= retentionFloor);
326
+ const bufferedPhysicalEvents = new Set(sourceEvents.map(patternPhysicalIdentity)).size;
327
+ if (bufferedPhysicalEvents > pattern.execution.maxBufferedEvents) {
328
+ await this.store.appendAudit({
329
+ auditId: `audit_${(await sha256Hex(`${definition.triggerId}:${definition.version}:pattern_buffer_overflow:${key ?? '-'}:${nowIso}`)).slice(0, 24)}`,
330
+ traceId: event.traceId,
331
+ timestamp: nowIso,
332
+ kind: 'event.buffer_overflow',
333
+ entityType: 'trigger',
334
+ entityId: definition.triggerId,
335
+ details: {
336
+ triggerVersion: definition.version,
337
+ patternVersion: '2',
338
+ partitionKey: key,
339
+ bufferedEvents: bufferedPhysicalEvents,
340
+ maxBufferedEvents: pattern.execution.maxBufferedEvents,
341
+ },
241
342
  });
242
- return {
243
- triggerId: definition.triggerId,
244
- match: record,
245
- matched: false,
246
- fired: false,
247
- };
343
+ const error = new Error(`Pattern v2 buffer exceeded maxBufferedEvents=${pattern.execution.maxBufferedEvents}`);
344
+ error.code =
345
+ 'PATTERN_V2_BUFFER_LIMIT_EXCEEDED';
346
+ throw error;
248
347
  }
249
- await this.support.cancelDeadlinesForMatch(record.matchId);
250
- if (definition.correlation?.semantic) {
251
- if (!this.evaluator) {
252
- await this.store.appendTriggerMatch(record);
253
- await this.support.auditMatch(record, 'trigger.partial', {
254
- reason: 'semantic_evaluator_unavailable',
255
- });
256
- return {
257
- triggerId: definition.triggerId,
258
- match: record,
259
- matched: false,
260
- fired: false,
261
- };
262
- }
263
- const semantic = definition.correlation.semantic;
264
- const decision = await new SemanticConditionEngine(this.evaluator).evaluate(semanticSource(record), {
265
- type: 'semantic_boolean',
266
- instruction: semantic.instruction,
267
- input: semantic.input,
268
- matchThreshold: semantic.matchThreshold,
269
- rejectThreshold: semantic.rejectThreshold,
270
- uncertain: semantic.uncertain,
271
- });
272
- record = TriggerMatchRecordSchema.parse({
273
- ...record,
274
- correlationDecision: {
275
- evaluator: decision.evaluator,
276
- outcome: decision.outcome,
277
- probability: decision.probability,
278
- matched: decision.matched,
279
- shouldEscalate: decision.shouldEscalate,
280
- inputFields: semantic.input,
281
- ...(decision.metadata ? { providerEvidence: decision.metadata } : {}),
282
- evaluatedAt: nowIso,
348
+ if (Date.parse(event.occurredAt) < retentionFloor &&
349
+ !existing?.sourceEvents.some((source) => sameEventIdentity(source, event))) {
350
+ await this.store.appendAudit({
351
+ auditId: `audit_${(await sha256Hex(`${definition.triggerId}:${definition.version}:pattern_late_event_dropped:${event.sourceEventId}:${nowIso}`)).slice(0, 24)}`,
352
+ traceId: event.traceId,
353
+ timestamp: nowIso,
354
+ kind: 'event.late_dropped',
355
+ entityType: 'trigger',
356
+ entityId: definition.triggerId,
357
+ details: {
358
+ triggerVersion: definition.version,
359
+ patternVersion: '2',
360
+ sourceEventId: event.sourceEventId,
361
+ occurredAt: event.occurredAt,
362
+ watermarkAt: new Date(watermarkMs).toISOString(),
363
+ retentionFloor: new Date(retentionFloor).toISOString(),
283
364
  },
284
- updatedAt: nowIso,
285
365
  });
286
- await this.store.appendTriggerMatch(record);
287
- await this.support.auditMatch(record, 'trigger.correlation', {
288
- outcome: decision.outcome,
289
- probability: decision.probability,
290
- evaluator: decision.evaluator,
291
- });
292
- if (!decision.matched) {
293
- return {
366
+ return [{
294
367
  triggerId: definition.triggerId,
295
- match: record,
368
+ match: existing ?? null,
296
369
  matched: false,
297
370
  fired: false,
298
- };
299
- }
371
+ }];
300
372
  }
301
- const matched = TriggerMatchRecordSchema.parse({
302
- ...record,
303
- status: 'matched',
304
- updatedAt: nowIso,
305
- });
306
- await this.store.appendTriggerMatch(matched);
307
- await this.support.auditMatch(matched, 'trigger.matched', {
308
- sourceEventIds: matched.sourceEvents.map((item) => item.sourceEventId),
309
- temporal: temporal.conditionStates,
310
- });
311
- return {
312
- triggerId: definition.triggerId,
313
- match: matched,
314
- matched: true,
315
- fired: false,
316
- };
317
- }
318
- async newMatch(definition, key, event) {
319
- const openedAt = event.occurredAt;
320
- const expiresAt = new Date(Date.parse(openedAt) + definition.withinMs).toISOString();
321
- const matchId = `tm_${(await sha256Hex([
322
- definition.triggerId,
323
- definition.version,
324
- key ?? '-',
325
- event.sourceEventId,
326
- ].join(':'))).slice(0, 24)}`;
327
- return TriggerMatchRecordSchema.parse({
373
+ const openedAt = sourceEvents[0]?.occurredAt ?? event.occurredAt;
374
+ const buffer = TriggerMatchRecordSchema.parse({
328
375
  protocolVersion: COMPOSITE_TRIGGER_PROTOCOL_VERSION,
329
376
  schemaVersion: COMPOSITE_TRIGGER_SCHEMA_VERSION,
330
- matchId,
377
+ matchId: bufferId,
331
378
  triggerId: definition.triggerId,
332
379
  triggerVersion: definition.version,
333
380
  status: 'partial',
334
- correlationKey: key,
381
+ partitionKey: key,
335
382
  openedAt,
336
- expiresAt,
337
- updatedAt: this.now().toISOString(),
338
- sourceEvents: [],
339
- correlationDecision: null,
383
+ expiresAt: new Date(Date.parse(openedAt) + definition.withinMs).toISOString(),
384
+ updatedAt: nowIso,
385
+ sourceEvents,
386
+ semanticDecision: null,
387
+ patternState: {
388
+ version: '2',
389
+ role: 'buffer',
390
+ maxObservedOccurredAt: new Date(maxObserved).toISOString(),
391
+ semanticDecisions: [],
392
+ },
340
393
  firedWakeId: null,
341
394
  derivedEventIds: [],
342
395
  });
396
+ await this.store.appendTriggerMatch(buffer);
397
+ return this.evaluatePatternBuffer(definition, buffer, now);
398
+ }
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;
411
+ if (!pattern)
412
+ return [];
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
+ : [];
428
+ const evaluation = await evaluatePatternV2({
429
+ definition: pattern,
430
+ events: buffer.sourceEvents,
431
+ evaluator: this.evaluator,
432
+ now: evaluationNow,
433
+ allowedLatenessMs: definition.eventTime?.allowedLatenessMs ?? 0,
434
+ semanticCache,
435
+ semanticCacheNamespace: `${definition.triggerId}@${definition.version}`,
436
+ });
437
+ for (const semantic of evaluation.semanticTrace) {
438
+ await this.support.auditMatch(buffer, 'trigger.correlation', {
439
+ patternVersion: '2',
440
+ semanticNodeId: semantic.nodeId,
441
+ candidateSignature: semantic.candidateSignature,
442
+ evaluator: semantic.decision.evaluator,
443
+ outcome: semantic.decision.outcome,
444
+ probability: semantic.decision.probability,
445
+ matched: semantic.decision.matched,
446
+ shouldEscalate: semantic.decision.shouldEscalate,
447
+ semanticCacheHits: evaluation.semanticCacheHits,
448
+ semanticCacheMisses: evaluation.semanticCacheMisses,
449
+ inputFields: semantic.decision.condition.input,
450
+ ...(semantic.decision.metadata
451
+ ? { providerEvidence: semantic.decision.metadata }
452
+ : {}),
453
+ });
454
+ }
455
+ await this.support.cancelDeadlinesForMatch(buffer.matchId);
456
+ if (evaluation.pending.length) {
457
+ const deadlines = evaluation.pending
458
+ .filter((candidate) => candidate.pendingUntil &&
459
+ this.candidateFitsTriggerWindow(candidate, definition.withinMs))
460
+ .map((candidate) => ({
461
+ conditionId: `pattern-v2:${patternV2CandidateSignature(candidate)}`,
462
+ dueAt: candidate.pendingUntil,
463
+ }));
464
+ await this.support.scheduleDeadlines(definition, buffer, deadlines);
465
+ }
466
+ const previouslyEmitted = new Set((await this.store
467
+ .listTriggerMatches(definition.triggerId))
468
+ .filter((record) => record.triggerVersion === definition.version &&
469
+ record.patternState?.version === '2' &&
470
+ record.patternState.role === 'match' &&
471
+ record.patternState.signature)
472
+ .map((record) => record.patternState.signature));
473
+ const fresh = evaluation.matches
474
+ .filter((candidate) => this.candidateFitsTriggerWindow(candidate, definition.withinMs))
475
+ .map((candidate, rank) => ({ candidate, rank }))
476
+ .filter(({ candidate }) => candidate.events.length > 0 &&
477
+ !previouslyEmitted.has(patternV2CandidateSignature(candidate)))
478
+ .sort((left, right) => left.candidate.startIndex - right.candidate.startIndex ||
479
+ left.rank - right.rank)
480
+ .map(({ candidate }) => candidate);
481
+ const selected = [];
482
+ const occupied = new Set();
483
+ let selectionLimit = pattern.selection.maxMatchesPerEvent;
484
+ const hasEffect = Boolean(definition.target || definition.derivedEvent);
485
+ if (hasEffect) {
486
+ const triggerState = this.store.getTriggerState
487
+ ? await this.store.getTriggerState(definition.triggerId, definition.version)
488
+ : null;
489
+ const lifecycle = definition.lifecycle ?? {};
490
+ if (lifecycle.oneShot || lifecycle.completeOnGoal) {
491
+ selectionLimit = Math.min(selectionLimit, 1);
492
+ }
493
+ if (lifecycle.maxFirings !== undefined) {
494
+ selectionLimit = Math.min(selectionLimit, Math.max(0, lifecycle.maxFirings - (triggerState?.fireCount ?? 0)));
495
+ }
496
+ if ((lifecycle.cooldownMs ?? 0) > 0) {
497
+ selectionLimit = Math.min(selectionLimit, 1);
498
+ }
499
+ }
500
+ const indexed = [...buffer.sourceEvents].sort(sourceEventOrder);
501
+ const indexByIdentity = new Map(indexed.map((source, index) => [
502
+ patternPhysicalIdentity(source),
503
+ index,
504
+ ]));
505
+ let skipPastLastThrough = -1;
506
+ let skipBefore = -1;
507
+ const skipStarts = new Set();
508
+ const bindingBoundary = (candidate, ref, edge) => {
509
+ const events = [...(candidate.bindings[ref] ?? [])]
510
+ .sort(sourceEventOrder);
511
+ const event = edge === 'first' ? events[0] : events.at(-1);
512
+ if (!event)
513
+ return null;
514
+ return indexByIdentity.get(patternPhysicalIdentity(event)) ?? null;
515
+ };
516
+ for (const candidate of fresh) {
517
+ if (selected.length >= selectionLimit)
518
+ break;
519
+ if (candidate.startIndex <= skipPastLastThrough)
520
+ continue;
521
+ if (candidate.startIndex < skipBefore)
522
+ continue;
523
+ if (skipStarts.has(candidate.startIndex))
524
+ continue;
525
+ const identities = candidate.events.map(patternPhysicalIdentity);
526
+ if (pattern.selection.overlap === 'disallow' &&
527
+ identities.some((identity) => occupied.has(identity))) {
528
+ continue;
529
+ }
530
+ selected.push(candidate);
531
+ identities.forEach((identity) => occupied.add(identity));
532
+ const afterMatch = pattern.selection.afterMatch;
533
+ if (afterMatch === 'skipPastLast') {
534
+ skipPastLastThrough = Math.max(skipPastLastThrough, candidate.endIndex);
535
+ }
536
+ else if (afterMatch === 'skipToNext') {
537
+ skipStarts.add(candidate.startIndex);
538
+ }
539
+ else if (typeof afterMatch === 'object') {
540
+ const boundary = bindingBoundary(candidate, afterMatch.ref, afterMatch.kind === 'skipToFirst' ? 'first' : 'last');
541
+ if (boundary !== null) {
542
+ skipBefore = Math.max(skipBefore, boundary);
543
+ }
544
+ }
545
+ }
546
+ const results = [];
547
+ for (const candidate of selected) {
548
+ const signature = patternV2CandidateSignature(candidate);
549
+ const matchId = `tm_${(await sha256Hex([
550
+ definition.triggerId,
551
+ definition.version,
552
+ buffer.partitionKey ?? '-',
553
+ signature,
554
+ 'pattern-v2-match',
555
+ ].join(':'))).slice(0, 24)}`;
556
+ const openedAt = candidate.events[0]?.occurredAt ?? buffer.openedAt;
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();
567
+ const lastSemantic = semanticDecisions.at(-1)?.decision ?? null;
568
+ const matched = TriggerMatchRecordSchema.parse({
569
+ protocolVersion: COMPOSITE_TRIGGER_PROTOCOL_VERSION,
570
+ schemaVersion: COMPOSITE_TRIGGER_SCHEMA_VERSION,
571
+ matchId,
572
+ triggerId: definition.triggerId,
573
+ triggerVersion: definition.version,
574
+ status: 'matched',
575
+ partitionKey: buffer.partitionKey,
576
+ openedAt,
577
+ expiresAt: new Date(Date.parse(openedAt) + definition.withinMs).toISOString(),
578
+ updatedAt: nowIso,
579
+ sourceEvents: candidate.events,
580
+ semanticDecision: lastSemantic,
581
+ patternState: {
582
+ version: '2',
583
+ role: 'match',
584
+ signature,
585
+ completedAt,
586
+ semanticDecisions,
587
+ },
588
+ firedWakeId: null,
589
+ derivedEventIds: [],
590
+ });
591
+ await this.store.appendTriggerMatch(matched);
592
+ await this.support.auditMatch(matched, 'trigger.matched', {
593
+ patternVersion: '2',
594
+ signature,
595
+ semanticEvaluations: evaluation.semanticEvaluations,
596
+ truncated: evaluation.truncated,
597
+ sourceEventIds: matched.sourceEvents.map((source) => source.sourceEventId),
598
+ });
599
+ results.push({
600
+ triggerId: definition.triggerId,
601
+ match: matched,
602
+ matched: true,
603
+ fired: false,
604
+ });
605
+ }
606
+ if (selected.length) {
607
+ const selectedIndexes = selected.flatMap((candidate) => [
608
+ candidate.startIndex,
609
+ candidate.endIndex,
610
+ ]).filter((index) => index >= 0);
611
+ const firstStart = selectedIndexes.length
612
+ ? Math.min(...selected.map((candidate) => candidate.startIndex))
613
+ : -1;
614
+ const lastEnd = selectedIndexes.length
615
+ ? Math.max(...selected.map((candidate) => candidate.endIndex))
616
+ : -1;
617
+ let retained = [...buffer.sourceEvents];
618
+ const shouldDropByIndex = (index) => {
619
+ const afterMatch = pattern.selection.afterMatch;
620
+ if (afterMatch === 'skipPastLast') {
621
+ return index <= lastEnd;
622
+ }
623
+ if (afterMatch === 'skipToNext') {
624
+ return index <= firstStart;
625
+ }
626
+ if (typeof afterMatch === 'object') {
627
+ const boundaries = selected
628
+ .map((candidate) => bindingBoundary(candidate, afterMatch.ref, afterMatch.kind === 'skipToFirst' ? 'first' : 'last'))
629
+ .filter((value) => value !== null);
630
+ if (!boundaries.length)
631
+ return false;
632
+ return index < Math.max(...boundaries);
633
+ }
634
+ return false;
635
+ };
636
+ const consumed = pattern.selection.overlap === 'disallow'
637
+ ? new Set(selected
638
+ .flatMap((candidate) => candidate.events)
639
+ .map(patternPhysicalIdentity))
640
+ : new Set();
641
+ retained = indexed.filter((source, index) => !shouldDropByIndex(index) &&
642
+ !consumed.has(patternPhysicalIdentity(source)));
643
+ const openedAt = retained[0]?.occurredAt ?? evaluationNow.toISOString();
644
+ const updatedBuffer = TriggerMatchRecordSchema.parse({
645
+ ...buffer,
646
+ openedAt,
647
+ expiresAt: new Date(Date.parse(openedAt) + definition.withinMs).toISOString(),
648
+ updatedAt: nowIso,
649
+ sourceEvents: retained,
650
+ });
651
+ await this.store.appendTriggerMatch(updatedBuffer);
652
+ }
653
+ if (!results.length) {
654
+ await this.support.auditMatch(buffer, 'trigger.partial', {
655
+ reason: 'pattern_v2_wait',
656
+ patternVersion: '2',
657
+ pendingCandidates: evaluation.pending.length,
658
+ semanticEvaluations: evaluation.semanticEvaluations,
659
+ truncated: evaluation.truncated,
660
+ });
661
+ return [{
662
+ triggerId: definition.triggerId,
663
+ match: buffer,
664
+ matched: false,
665
+ fired: false,
666
+ }];
667
+ }
668
+ return results;
343
669
  }
344
- latestMatch(matchId) {
345
- return this.store
346
- .listTriggerMatches()
670
+ async latestMatch(matchId) {
671
+ return (await this.store.listTriggerMatches())
347
672
  .filter((record) => record.matchId === matchId)
348
673
  .sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0] ?? null;
349
674
  }