mcp-event-intelligence 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/LICENSE +201 -0
- package/README.md +298 -0
- package/SECURITY.md +42 -0
- package/bin/mcp-event-intelligence.mjs +52 -0
- package/dist/src/composite/engine.d.ts +87 -0
- package/dist/src/composite/engine.js +667 -0
- package/dist/src/composite/temporal.d.ts +32 -0
- package/dist/src/composite/temporal.js +317 -0
- package/dist/src/intelligenceProtocol/audit.d.ts +19 -0
- package/dist/src/intelligenceProtocol/audit.js +56 -0
- package/dist/src/intelligenceProtocol/canonical.d.ts +2 -0
- package/dist/src/intelligenceProtocol/canonical.js +21 -0
- package/dist/src/intelligenceProtocol/index.d.ts +12 -0
- package/dist/src/intelligenceProtocol/index.js +8 -0
- package/dist/src/intelligenceProtocol/invariants.d.ts +3 -0
- package/dist/src/intelligenceProtocol/invariants.js +36 -0
- package/dist/src/intelligenceProtocol/lifecycle.d.ts +4 -0
- package/dist/src/intelligenceProtocol/lifecycle.js +24 -0
- package/dist/src/intelligenceProtocol/mapper.d.ts +14 -0
- package/dist/src/intelligenceProtocol/mapper.js +28 -0
- package/dist/src/intelligenceProtocol/schemas.d.ts +325 -0
- package/dist/src/intelligenceProtocol/schemas.js +182 -0
- package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +488 -0
- package/dist/src/intelligenceProtocol/triggerSchemas.js +379 -0
- package/dist/src/intelligenceProtocol/version.d.ts +7 -0
- package/dist/src/intelligenceProtocol/version.js +7 -0
- package/dist/src/mcpEvents/consumer.d.ts +7 -0
- package/dist/src/mcpEvents/consumer.js +14 -0
- package/dist/src/mcpEvents/server.d.ts +41 -0
- package/dist/src/mcpEvents/server.js +122 -0
- package/dist/src/protocol/types.d.ts +37 -0
- package/dist/src/protocol/types.js +1 -0
- package/dist/src/semantic/conditionEngine.d.ts +7 -0
- package/dist/src/semantic/conditionEngine.js +65 -0
- package/dist/src/semantic/types.d.ts +31 -0
- package/dist/src/semantic/types.js +1 -0
- package/dist/src/semantic/typesafeJevEvaluator.d.ts +23 -0
- package/dist/src/semantic/typesafeJevEvaluator.js +95 -0
- package/package.json +85 -0
- package/scripts/host-integration.d.mts +86 -0
- package/scripts/host-integration.mjs +216 -0
- package/scripts/lib/composite-event-consumer.mjs +172 -0
- package/scripts/lib/composite-wake-coordinator.mjs +185 -0
- package/scripts/lib/derived-contract-registry.mjs +210 -0
- package/scripts/lib/derived-event-coordinator.mjs +242 -0
- package/scripts/lib/event-processor.mjs +409 -0
- package/scripts/lib/generic-runtime-wake.mjs +76 -0
- package/scripts/lib/github-mcp-events-adapter.mjs +66 -0
- package/scripts/lib/github-webhook.mjs +88 -0
- package/scripts/lib/http-wake-deliverer.mjs +31 -0
- package/scripts/lib/local-event-intelligence-runtime.mjs +193 -0
- package/scripts/lib/mcp-events-client.mjs +519 -0
- package/scripts/lib/persistent-event-store.mjs +769 -0
- package/scripts/lib/temporal-deadline-scheduler.mjs +111 -0
- package/scripts/lib/trigger-control-plane.mjs +939 -0
- package/scripts/lib/trigger-inspector.mjs +631 -0
- package/scripts/lib/wake-signature.mjs +10 -0
- package/scripts/mcp-stdio-server.mjs +380 -0
- package/scripts/mcp-stdio.mjs +3 -0
- package/scripts/run-tests.mjs +51 -0
- package/scripts/service.mjs +756 -0
- package/server.json +68 -0
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
import { SemanticConditionEngine } from '../semantic/conditionEngine.js';
|
|
2
|
+
import { sha256Hex } from '../intelligenceProtocol/canonical.js';
|
|
3
|
+
import { COMPOSITE_TRIGGER_PROTOCOL_VERSION, COMPOSITE_TRIGGER_SCHEMA_VERSION, CompositeTriggerDefinitionSchema, CorrelatableEventSchema, TriggerMatchRecordSchema, } from '../intelligenceProtocol/triggerSchemas.js';
|
|
4
|
+
import { evaluateTemporalConditions, } from './temporal.js';
|
|
5
|
+
function getByPath(source, path) {
|
|
6
|
+
let current = source;
|
|
7
|
+
for (const part of path.split('.')) {
|
|
8
|
+
if (current === null ||
|
|
9
|
+
typeof current !== 'object' ||
|
|
10
|
+
Array.isArray(current))
|
|
11
|
+
return undefined;
|
|
12
|
+
current = current[part];
|
|
13
|
+
}
|
|
14
|
+
return current;
|
|
15
|
+
}
|
|
16
|
+
function scalarKey(value) {
|
|
17
|
+
if (typeof value === 'string' ||
|
|
18
|
+
typeof value === 'number' ||
|
|
19
|
+
typeof value === 'boolean')
|
|
20
|
+
return String(value);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
function predicateMatches(event, predicate) {
|
|
24
|
+
const value = getByPath(event.data, predicate.path);
|
|
25
|
+
switch (predicate.op) {
|
|
26
|
+
case 'eq':
|
|
27
|
+
return value === predicate.value;
|
|
28
|
+
case 'neq':
|
|
29
|
+
return value !== predicate.value;
|
|
30
|
+
case 'contains':
|
|
31
|
+
if (typeof value === 'string')
|
|
32
|
+
return value.includes(String(predicate.value));
|
|
33
|
+
if (Array.isArray(value))
|
|
34
|
+
return value.includes(predicate.value);
|
|
35
|
+
return false;
|
|
36
|
+
case 'in':
|
|
37
|
+
return predicate.value.includes(value);
|
|
38
|
+
case 'exists':
|
|
39
|
+
return predicate.value ? value !== undefined : value === undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function clauseMatches(clause, event) {
|
|
43
|
+
const contractVersion = getByPath(event.data, '_derived.contractVersion');
|
|
44
|
+
return (clause.event === event.name &&
|
|
45
|
+
(!clause.serverId || clause.serverId === event.serverId) &&
|
|
46
|
+
(!clause.contractVersion ||
|
|
47
|
+
String(contractVersion ?? '') === String(clause.contractVersion)) &&
|
|
48
|
+
clause.where.every((predicate) => predicateMatches(event, predicate)));
|
|
49
|
+
}
|
|
50
|
+
function asSourceEvent(clauseId, event) {
|
|
51
|
+
return {
|
|
52
|
+
clauseId,
|
|
53
|
+
traceId: event.traceId,
|
|
54
|
+
sourceEventId: event.sourceEventId,
|
|
55
|
+
eventName: event.name,
|
|
56
|
+
occurredAt: event.occurredAt,
|
|
57
|
+
...(event.provider ? { provider: event.provider } : {}),
|
|
58
|
+
...(event.serverId ? { serverId: event.serverId } : {}),
|
|
59
|
+
...(event.payloadHash ? { payloadHash: event.payloadHash } : {}),
|
|
60
|
+
data: event.data,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function clauseEvents(record, ref) {
|
|
64
|
+
return record.sourceEvents
|
|
65
|
+
.filter((event) => event.clauseId === ref)
|
|
66
|
+
.sort((a, b) => Date.parse(a.occurredAt) - Date.parse(b.occurredAt));
|
|
67
|
+
}
|
|
68
|
+
function expressionSatisfied(definition, record) {
|
|
69
|
+
const expression = definition.expression;
|
|
70
|
+
if (expression.kind === 'allOf') {
|
|
71
|
+
return expression.refs.every((ref) => clauseEvents(record, ref).length > 0);
|
|
72
|
+
}
|
|
73
|
+
if (expression.kind === 'anyOf') {
|
|
74
|
+
return expression.refs.some((ref) => clauseEvents(record, ref).length > 0);
|
|
75
|
+
}
|
|
76
|
+
if (expression.kind === 'count') {
|
|
77
|
+
return clauseEvents(record, expression.ref).length >= expression.atLeast;
|
|
78
|
+
}
|
|
79
|
+
let previous = Number.NEGATIVE_INFINITY;
|
|
80
|
+
for (const ref of expression.refs) {
|
|
81
|
+
const candidate = clauseEvents(record, ref)
|
|
82
|
+
.find((event) => Date.parse(event.occurredAt) > previous);
|
|
83
|
+
if (!candidate)
|
|
84
|
+
return false;
|
|
85
|
+
previous = Date.parse(candidate.occurredAt);
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
function semanticSource(record) {
|
|
90
|
+
const source = {};
|
|
91
|
+
for (const event of record.sourceEvents) {
|
|
92
|
+
source[event.clauseId] = event.data;
|
|
93
|
+
}
|
|
94
|
+
return source;
|
|
95
|
+
}
|
|
96
|
+
function deterministicKeyForClause(definition, clauseId, event) {
|
|
97
|
+
const deterministic = definition.correlation?.deterministic;
|
|
98
|
+
if (!deterministic)
|
|
99
|
+
return null;
|
|
100
|
+
const field = deterministic.fields.find((item) => item.ref === clauseId);
|
|
101
|
+
if (!field)
|
|
102
|
+
return null;
|
|
103
|
+
return scalarKey(getByPath(event.data, field.path));
|
|
104
|
+
}
|
|
105
|
+
function sameEventIdentity(source, event) {
|
|
106
|
+
return (source.sourceEventId === event.sourceEventId &&
|
|
107
|
+
(source.serverId ?? null) === (event.serverId ?? null));
|
|
108
|
+
}
|
|
109
|
+
function matchesCorrelationKey(record, key) {
|
|
110
|
+
if (record.correlationKey === null || key === null)
|
|
111
|
+
return true;
|
|
112
|
+
return record.correlationKey === key;
|
|
113
|
+
}
|
|
114
|
+
const DERIVED_SERVER_ID = 'event-intelligence:derived';
|
|
115
|
+
function eventNode(eventName, serverId) {
|
|
116
|
+
return `${serverId || DERIVED_SERVER_ID}::${eventName}`;
|
|
117
|
+
}
|
|
118
|
+
function assertNoDerivedEventCycle(existing, candidate, getState) {
|
|
119
|
+
const definitions = [
|
|
120
|
+
...existing.filter((definition) => {
|
|
121
|
+
const state = getState?.(definition.triggerId, definition.version);
|
|
122
|
+
return !state || state.status === 'active' || state.status === 'paused';
|
|
123
|
+
}),
|
|
124
|
+
candidate,
|
|
125
|
+
].filter((definition) => definition.derivedEvent);
|
|
126
|
+
const graph = new Map();
|
|
127
|
+
for (const definition of definitions) {
|
|
128
|
+
const output = eventNode(definition.derivedEvent.name, DERIVED_SERVER_ID);
|
|
129
|
+
for (const clause of definition.clauses) {
|
|
130
|
+
const input = eventNode(clause.event, clause.serverId);
|
|
131
|
+
const edges = graph.get(input) ?? new Set();
|
|
132
|
+
edges.add(output);
|
|
133
|
+
graph.set(input, edges);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const visiting = new Set();
|
|
137
|
+
const visited = new Set();
|
|
138
|
+
const dfs = (node) => {
|
|
139
|
+
if (visiting.has(node))
|
|
140
|
+
return true;
|
|
141
|
+
if (visited.has(node))
|
|
142
|
+
return false;
|
|
143
|
+
visiting.add(node);
|
|
144
|
+
for (const next of graph.get(node) ?? []) {
|
|
145
|
+
if (dfs(next))
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
visiting.delete(node);
|
|
149
|
+
visited.add(node);
|
|
150
|
+
return false;
|
|
151
|
+
};
|
|
152
|
+
for (const node of graph.keys()) {
|
|
153
|
+
if (dfs(node)) {
|
|
154
|
+
const error = new Error(`Derived event graph contains a cycle involving ${node}`);
|
|
155
|
+
error.code = 'TRIGGER_DERIVED_EVENT_CYCLE';
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export class CompositeTriggerEngine {
|
|
161
|
+
store;
|
|
162
|
+
evaluator;
|
|
163
|
+
now;
|
|
164
|
+
constructor(store, evaluator = null, now = () => new Date()) {
|
|
165
|
+
this.store = store;
|
|
166
|
+
this.evaluator = evaluator;
|
|
167
|
+
this.now = now;
|
|
168
|
+
}
|
|
169
|
+
async register(definitionInput) {
|
|
170
|
+
const definition = CompositeTriggerDefinitionSchema.parse(definitionInput);
|
|
171
|
+
assertNoDerivedEventCycle(this.store.listTriggers(), definition, this.store.getTriggerState?.bind(this.store));
|
|
172
|
+
await this.store.putTrigger(definition);
|
|
173
|
+
return definition;
|
|
174
|
+
}
|
|
175
|
+
async ingest(eventInput) {
|
|
176
|
+
const event = CorrelatableEventSchema.parse(eventInput);
|
|
177
|
+
if (event.name === 'event-intelligence.timer.reached') {
|
|
178
|
+
return this.ingestTimerEvent(event);
|
|
179
|
+
}
|
|
180
|
+
const results = [];
|
|
181
|
+
for (const definition of this.store.listTriggers()) {
|
|
182
|
+
const triggerState = this.store.getTriggerState?.(definition.triggerId, definition.version) ?? null;
|
|
183
|
+
if (!(await this.lifecycleAllows(definition, triggerState, this.now()))) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const matchingClauses = definition.clauses.filter((clause) => clauseMatches(clause, event));
|
|
187
|
+
if (matchingClauses.length === 0)
|
|
188
|
+
continue;
|
|
189
|
+
const replayed = this.store
|
|
190
|
+
.listTriggerMatches(definition.triggerId)
|
|
191
|
+
.find((record) => record.sourceEvents.some((source) => sameEventIdentity(source, event)));
|
|
192
|
+
if (replayed) {
|
|
193
|
+
results.push({
|
|
194
|
+
triggerId: definition.triggerId,
|
|
195
|
+
match: replayed,
|
|
196
|
+
matched: replayed.status === 'matched' ||
|
|
197
|
+
replayed.status === 'emitted' ||
|
|
198
|
+
replayed.status === 'fired',
|
|
199
|
+
fired: replayed.status === 'fired',
|
|
200
|
+
});
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const clause = matchingClauses[0];
|
|
204
|
+
results.push(await this.applyEvent(definition, clause, event));
|
|
205
|
+
}
|
|
206
|
+
return results;
|
|
207
|
+
}
|
|
208
|
+
async markFired(matchId, wakeId) {
|
|
209
|
+
const latest = this.latestMatch(matchId);
|
|
210
|
+
if (!latest)
|
|
211
|
+
throw new Error(`Unknown trigger match: ${matchId}`);
|
|
212
|
+
if (latest.status === 'fired')
|
|
213
|
+
return latest;
|
|
214
|
+
if (latest.status !== 'matched') {
|
|
215
|
+
throw new Error(`Trigger match must be matched before firing; current=${latest.status}`);
|
|
216
|
+
}
|
|
217
|
+
const fired = TriggerMatchRecordSchema.parse({
|
|
218
|
+
...latest,
|
|
219
|
+
status: 'fired',
|
|
220
|
+
firedWakeId: wakeId,
|
|
221
|
+
updatedAt: this.now().toISOString(),
|
|
222
|
+
});
|
|
223
|
+
await this.store.appendTriggerMatch(fired);
|
|
224
|
+
await this.cancelDeadlinesForMatch(matchId);
|
|
225
|
+
await this.audit(fired, 'trigger.fired', { wakeId });
|
|
226
|
+
await this.advanceLifecycleAfterEffect(fired, 'wake', wakeId);
|
|
227
|
+
return fired;
|
|
228
|
+
}
|
|
229
|
+
async markDerivedEmitted(matchId, derivedEventId) {
|
|
230
|
+
const latest = this.latestMatch(matchId);
|
|
231
|
+
if (!latest)
|
|
232
|
+
throw new Error(`Unknown trigger match: ${matchId}`);
|
|
233
|
+
if (latest.status === 'emitted' &&
|
|
234
|
+
latest.derivedEventIds.includes(derivedEventId)) {
|
|
235
|
+
return latest;
|
|
236
|
+
}
|
|
237
|
+
if (latest.status !== 'matched') {
|
|
238
|
+
throw new Error(`Trigger match must be matched before derived emission; current=${latest.status}`);
|
|
239
|
+
}
|
|
240
|
+
const emitted = TriggerMatchRecordSchema.parse({
|
|
241
|
+
...latest,
|
|
242
|
+
status: 'emitted',
|
|
243
|
+
updatedAt: this.now().toISOString(),
|
|
244
|
+
derivedEventIds: [
|
|
245
|
+
...new Set([...latest.derivedEventIds, derivedEventId]),
|
|
246
|
+
],
|
|
247
|
+
});
|
|
248
|
+
await this.store.appendTriggerMatch(emitted);
|
|
249
|
+
await this.cancelDeadlinesForMatch(matchId);
|
|
250
|
+
await this.audit(emitted, 'trigger.emitted', {
|
|
251
|
+
derivedEventId,
|
|
252
|
+
});
|
|
253
|
+
await this.advanceLifecycleAfterEffect(emitted, 'derived_event', derivedEventId);
|
|
254
|
+
return emitted;
|
|
255
|
+
}
|
|
256
|
+
async ingestTimerEvent(event) {
|
|
257
|
+
const deadlineId = String(event.data.deadlineId ?? '');
|
|
258
|
+
if (!deadlineId || !this.store.getTemporalDeadline)
|
|
259
|
+
return [];
|
|
260
|
+
const deadline = this.store.getTemporalDeadline(deadlineId);
|
|
261
|
+
if (!deadline || deadline.status !== 'pending')
|
|
262
|
+
return [];
|
|
263
|
+
const definition = this.store.listTriggers().find((candidate) => candidate.triggerId === deadline.triggerId &&
|
|
264
|
+
candidate.version === deadline.triggerVersion);
|
|
265
|
+
if (!definition)
|
|
266
|
+
return [];
|
|
267
|
+
const triggerState = this.store.getTriggerState?.(definition.triggerId, definition.version) ?? null;
|
|
268
|
+
if (!(await this.lifecycleAllows(definition, triggerState, this.now()))) {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
const match = this.latestMatch(deadline.matchId);
|
|
272
|
+
if (!match ||
|
|
273
|
+
match.status === 'matched' ||
|
|
274
|
+
match.status === 'emitted' ||
|
|
275
|
+
match.status === 'fired' ||
|
|
276
|
+
match.status === 'expired') {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
if (Date.parse(event.occurredAt) > Date.parse(match.expiresAt)) {
|
|
280
|
+
const expired = TriggerMatchRecordSchema.parse({
|
|
281
|
+
...match,
|
|
282
|
+
status: 'expired',
|
|
283
|
+
updatedAt: event.occurredAt,
|
|
284
|
+
});
|
|
285
|
+
await this.store.appendTriggerMatch(expired);
|
|
286
|
+
await this.cancelDeadlinesForMatch(expired.matchId);
|
|
287
|
+
await this.audit(expired, 'trigger.expired', {
|
|
288
|
+
reason: 'temporal_deadline_after_event_window',
|
|
289
|
+
});
|
|
290
|
+
return [{
|
|
291
|
+
triggerId: definition.triggerId,
|
|
292
|
+
match: expired,
|
|
293
|
+
matched: false,
|
|
294
|
+
fired: false,
|
|
295
|
+
}];
|
|
296
|
+
}
|
|
297
|
+
const result = await this.finalizeEligible(definition, match, new Date(event.occurredAt));
|
|
298
|
+
return [result];
|
|
299
|
+
}
|
|
300
|
+
async applyEvent(definition, clause, event) {
|
|
301
|
+
const nowIso = this.now().toISOString();
|
|
302
|
+
const eventTime = Date.parse(event.occurredAt);
|
|
303
|
+
const key = deterministicKeyForClause(definition, clause.id, event);
|
|
304
|
+
const existing = this.store
|
|
305
|
+
.listTriggerMatches(definition.triggerId)
|
|
306
|
+
.filter((record) => record.triggerVersion === definition.version &&
|
|
307
|
+
(record.status === 'partial' || record.status === 'matched') &&
|
|
308
|
+
matchesCorrelationKey(record, key))
|
|
309
|
+
.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0];
|
|
310
|
+
if (existing?.status === 'matched') {
|
|
311
|
+
return {
|
|
312
|
+
triggerId: definition.triggerId,
|
|
313
|
+
match: existing,
|
|
314
|
+
matched: true,
|
|
315
|
+
fired: false,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
let record = existing ?? await this.newMatch(definition, key, event);
|
|
319
|
+
if (Date.parse(record.expiresAt) < eventTime) {
|
|
320
|
+
const expired = TriggerMatchRecordSchema.parse({
|
|
321
|
+
...record,
|
|
322
|
+
status: 'expired',
|
|
323
|
+
updatedAt: nowIso,
|
|
324
|
+
});
|
|
325
|
+
await this.store.appendTriggerMatch(expired);
|
|
326
|
+
await this.cancelDeadlinesForMatch(expired.matchId);
|
|
327
|
+
await this.audit(expired, 'trigger.expired', {
|
|
328
|
+
reason: 'window_expired_before_event',
|
|
329
|
+
});
|
|
330
|
+
record = await this.newMatch(definition, key, event);
|
|
331
|
+
}
|
|
332
|
+
const duplicate = record.sourceEvents.some((candidate) => sameEventIdentity(candidate, event));
|
|
333
|
+
if (!duplicate) {
|
|
334
|
+
record = TriggerMatchRecordSchema.parse({
|
|
335
|
+
...record,
|
|
336
|
+
correlationKey: record.correlationKey ?? key,
|
|
337
|
+
updatedAt: nowIso,
|
|
338
|
+
sourceEvents: [
|
|
339
|
+
...record.sourceEvents,
|
|
340
|
+
asSourceEvent(clause.id, event),
|
|
341
|
+
],
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (!expressionSatisfied(definition, record)) {
|
|
345
|
+
await this.store.appendTriggerMatch(record);
|
|
346
|
+
await this.audit(record, 'trigger.partial', {
|
|
347
|
+
clauseId: clause.id,
|
|
348
|
+
sourceEventId: event.sourceEventId,
|
|
349
|
+
});
|
|
350
|
+
return {
|
|
351
|
+
triggerId: definition.triggerId,
|
|
352
|
+
match: record,
|
|
353
|
+
matched: false,
|
|
354
|
+
fired: false,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
return this.finalizeEligible(definition, record, this.now());
|
|
358
|
+
}
|
|
359
|
+
async finalizeEligible(definition, record, evaluationNow) {
|
|
360
|
+
const nowIso = evaluationNow.toISOString();
|
|
361
|
+
const temporal = evaluateTemporalConditions(definition, record, evaluationNow);
|
|
362
|
+
if (temporal.outcome === 'blocked') {
|
|
363
|
+
const expired = TriggerMatchRecordSchema.parse({
|
|
364
|
+
...record,
|
|
365
|
+
status: 'expired',
|
|
366
|
+
updatedAt: nowIso,
|
|
367
|
+
});
|
|
368
|
+
await this.store.appendTriggerMatch(expired);
|
|
369
|
+
await this.cancelDeadlinesForMatch(expired.matchId);
|
|
370
|
+
await this.audit(expired, 'trigger.expired', {
|
|
371
|
+
reason: 'temporal_condition_blocked',
|
|
372
|
+
temporal: temporal.conditionStates,
|
|
373
|
+
});
|
|
374
|
+
return {
|
|
375
|
+
triggerId: definition.triggerId,
|
|
376
|
+
match: expired,
|
|
377
|
+
matched: false,
|
|
378
|
+
fired: false,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
if (temporal.outcome === 'pending') {
|
|
382
|
+
await this.store.appendTriggerMatch(TriggerMatchRecordSchema.parse({
|
|
383
|
+
...record,
|
|
384
|
+
status: 'partial',
|
|
385
|
+
updatedAt: nowIso,
|
|
386
|
+
}));
|
|
387
|
+
await this.scheduleDeadlines(definition, record, temporal.pendingDeadlines);
|
|
388
|
+
await this.audit(record, 'trigger.partial', {
|
|
389
|
+
reason: 'temporal_wait',
|
|
390
|
+
temporal: temporal.conditionStates,
|
|
391
|
+
});
|
|
392
|
+
return {
|
|
393
|
+
triggerId: definition.triggerId,
|
|
394
|
+
match: record,
|
|
395
|
+
matched: false,
|
|
396
|
+
fired: false,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
await this.cancelDeadlinesForMatch(record.matchId);
|
|
400
|
+
if (definition.correlation?.semantic) {
|
|
401
|
+
if (!this.evaluator) {
|
|
402
|
+
await this.store.appendTriggerMatch(record);
|
|
403
|
+
await this.audit(record, 'trigger.partial', {
|
|
404
|
+
reason: 'semantic_evaluator_unavailable',
|
|
405
|
+
});
|
|
406
|
+
return {
|
|
407
|
+
triggerId: definition.triggerId,
|
|
408
|
+
match: record,
|
|
409
|
+
matched: false,
|
|
410
|
+
fired: false,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
const semantic = definition.correlation.semantic;
|
|
414
|
+
const decision = await new SemanticConditionEngine(this.evaluator).evaluate(semanticSource(record), {
|
|
415
|
+
type: 'semantic_boolean',
|
|
416
|
+
instruction: semantic.instruction,
|
|
417
|
+
input: semantic.input,
|
|
418
|
+
matchThreshold: semantic.matchThreshold,
|
|
419
|
+
rejectThreshold: semantic.rejectThreshold,
|
|
420
|
+
uncertain: semantic.uncertain,
|
|
421
|
+
});
|
|
422
|
+
record = TriggerMatchRecordSchema.parse({
|
|
423
|
+
...record,
|
|
424
|
+
correlationDecision: {
|
|
425
|
+
evaluator: decision.evaluator,
|
|
426
|
+
outcome: decision.outcome,
|
|
427
|
+
probability: decision.probability,
|
|
428
|
+
matched: decision.matched,
|
|
429
|
+
shouldEscalate: decision.shouldEscalate,
|
|
430
|
+
inputFields: semantic.input,
|
|
431
|
+
...(decision.metadata ? { providerEvidence: decision.metadata } : {}),
|
|
432
|
+
evaluatedAt: nowIso,
|
|
433
|
+
},
|
|
434
|
+
updatedAt: nowIso,
|
|
435
|
+
});
|
|
436
|
+
await this.store.appendTriggerMatch(record);
|
|
437
|
+
await this.audit(record, 'trigger.correlation', {
|
|
438
|
+
outcome: decision.outcome,
|
|
439
|
+
probability: decision.probability,
|
|
440
|
+
evaluator: decision.evaluator,
|
|
441
|
+
});
|
|
442
|
+
if (!decision.matched) {
|
|
443
|
+
return {
|
|
444
|
+
triggerId: definition.triggerId,
|
|
445
|
+
match: record,
|
|
446
|
+
matched: false,
|
|
447
|
+
fired: false,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const matched = TriggerMatchRecordSchema.parse({
|
|
452
|
+
...record,
|
|
453
|
+
status: 'matched',
|
|
454
|
+
updatedAt: nowIso,
|
|
455
|
+
});
|
|
456
|
+
await this.store.appendTriggerMatch(matched);
|
|
457
|
+
await this.audit(matched, 'trigger.matched', {
|
|
458
|
+
sourceEventIds: matched.sourceEvents.map((item) => item.sourceEventId),
|
|
459
|
+
temporal: temporal.conditionStates,
|
|
460
|
+
});
|
|
461
|
+
return {
|
|
462
|
+
triggerId: definition.triggerId,
|
|
463
|
+
match: matched,
|
|
464
|
+
matched: true,
|
|
465
|
+
fired: false,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
async scheduleDeadlines(definition, record, deadlines) {
|
|
469
|
+
if (!this.store.putTemporalDeadline)
|
|
470
|
+
return;
|
|
471
|
+
for (const deadline of deadlines) {
|
|
472
|
+
const deadlineId = `td_${(await sha256Hex([
|
|
473
|
+
definition.triggerId,
|
|
474
|
+
definition.version,
|
|
475
|
+
record.matchId,
|
|
476
|
+
deadline.conditionId,
|
|
477
|
+
deadline.dueAt,
|
|
478
|
+
].join(':'))).slice(0, 24)}`;
|
|
479
|
+
const existing = this.store.getTemporalDeadline?.(deadlineId);
|
|
480
|
+
if (existing?.status === 'pending' || existing?.status === 'fired') {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
await this.store.putTemporalDeadline({
|
|
484
|
+
deadlineId,
|
|
485
|
+
triggerId: definition.triggerId,
|
|
486
|
+
triggerVersion: definition.version,
|
|
487
|
+
matchId: record.matchId,
|
|
488
|
+
conditionId: deadline.conditionId,
|
|
489
|
+
dueAt: deadline.dueAt,
|
|
490
|
+
status: 'pending',
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async cancelDeadlinesForMatch(matchId) {
|
|
495
|
+
if (!this.store.listTemporalDeadlines ||
|
|
496
|
+
!this.store.setTemporalDeadlineStatus)
|
|
497
|
+
return;
|
|
498
|
+
const pending = this.store.listTemporalDeadlines({
|
|
499
|
+
matchId,
|
|
500
|
+
status: 'pending',
|
|
501
|
+
});
|
|
502
|
+
for (const deadline of pending) {
|
|
503
|
+
await this.store.setTemporalDeadlineStatus(deadline.deadlineId, 'cancelled');
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
async advanceLifecycleAfterEffect(match, effectKind, effectId) {
|
|
507
|
+
const definition = this.store.listTriggers().find((candidate) => candidate.triggerId === match.triggerId &&
|
|
508
|
+
candidate.version === match.triggerVersion);
|
|
509
|
+
const triggerState = this.store.getTriggerState?.(match.triggerId, match.triggerVersion) ?? null;
|
|
510
|
+
if (!definition || !this.store.setTriggerState)
|
|
511
|
+
return;
|
|
512
|
+
const previousCount = triggerState?.fireCount ?? 0;
|
|
513
|
+
const fireCount = previousCount + 1;
|
|
514
|
+
const lifecycle = definition.lifecycle ?? {};
|
|
515
|
+
const complete = lifecycle.oneShot === true ||
|
|
516
|
+
lifecycle.completeOnGoal === true ||
|
|
517
|
+
(lifecycle.maxFirings !== undefined &&
|
|
518
|
+
fireCount >= lifecycle.maxFirings);
|
|
519
|
+
const nextStatus = complete ? 'completed' : 'active';
|
|
520
|
+
const effectAt = this.now().toISOString();
|
|
521
|
+
await this.store.setTriggerState(match.triggerId, match.triggerVersion, nextStatus, {
|
|
522
|
+
type: 'system',
|
|
523
|
+
principal_id: 'event-intelligence:lifecycle',
|
|
524
|
+
}, {
|
|
525
|
+
owner: triggerState?.owner ?? null,
|
|
526
|
+
connectionIds: triggerState?.connectionIds ?? [],
|
|
527
|
+
fireCount,
|
|
528
|
+
lastFiredAt: effectAt,
|
|
529
|
+
});
|
|
530
|
+
if (complete) {
|
|
531
|
+
await this.auditTriggerLifecycle(definition, 'trigger.completed', {
|
|
532
|
+
reason: lifecycle.oneShot
|
|
533
|
+
? 'one_shot'
|
|
534
|
+
: lifecycle.completeOnGoal
|
|
535
|
+
? 'complete_on_goal'
|
|
536
|
+
: 'max_firings',
|
|
537
|
+
fireCount,
|
|
538
|
+
effectKind,
|
|
539
|
+
effectId,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
async lifecycleAllows(definition, state, now) {
|
|
544
|
+
const lifecycle = definition.lifecycle ?? {};
|
|
545
|
+
const effectiveState = state ?? {
|
|
546
|
+
status: 'active',
|
|
547
|
+
fireCount: 0,
|
|
548
|
+
lastFiredAt: null,
|
|
549
|
+
connectionIds: [],
|
|
550
|
+
owner: null,
|
|
551
|
+
};
|
|
552
|
+
if (effectiveState.status !== 'active')
|
|
553
|
+
return false;
|
|
554
|
+
const expiresAt = lifecycle.expiresAt
|
|
555
|
+
? Date.parse(lifecycle.expiresAt)
|
|
556
|
+
: Number.POSITIVE_INFINITY;
|
|
557
|
+
const leaseUntil = lifecycle.leaseUntil
|
|
558
|
+
? Date.parse(lifecycle.leaseUntil)
|
|
559
|
+
: Number.POSITIVE_INFINITY;
|
|
560
|
+
if (now.getTime() > Math.min(expiresAt, leaseUntil)) {
|
|
561
|
+
if (this.store.setTriggerState) {
|
|
562
|
+
await this.store.setTriggerState(definition.triggerId, definition.version, 'expired', {
|
|
563
|
+
type: 'system',
|
|
564
|
+
principal_id: 'event-intelligence:lifecycle',
|
|
565
|
+
}, {
|
|
566
|
+
owner: effectiveState.owner ?? null,
|
|
567
|
+
connectionIds: effectiveState.connectionIds ?? [],
|
|
568
|
+
fireCount: effectiveState.fireCount ?? 0,
|
|
569
|
+
lastFiredAt: effectiveState.lastFiredAt ?? null,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
await this.auditTriggerLifecycle(definition, 'trigger.lifecycle_expired', {
|
|
573
|
+
expiresAt: lifecycle.expiresAt ?? null,
|
|
574
|
+
leaseUntil: lifecycle.leaseUntil ?? null,
|
|
575
|
+
});
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
const fireCount = effectiveState.fireCount ?? 0;
|
|
579
|
+
if ((lifecycle.oneShot && fireCount >= 1) ||
|
|
580
|
+
(lifecycle.maxFirings !== undefined &&
|
|
581
|
+
fireCount >= lifecycle.maxFirings)) {
|
|
582
|
+
if (this.store.setTriggerState) {
|
|
583
|
+
await this.store.setTriggerState(definition.triggerId, definition.version, 'completed', {
|
|
584
|
+
type: 'system',
|
|
585
|
+
principal_id: 'event-intelligence:lifecycle',
|
|
586
|
+
}, {
|
|
587
|
+
owner: effectiveState.owner ?? null,
|
|
588
|
+
connectionIds: effectiveState.connectionIds ?? [],
|
|
589
|
+
fireCount,
|
|
590
|
+
lastFiredAt: effectiveState.lastFiredAt ?? null,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
if (lifecycle.cooldownMs > 0 &&
|
|
596
|
+
effectiveState.lastFiredAt &&
|
|
597
|
+
now.getTime() - Date.parse(effectiveState.lastFiredAt) <
|
|
598
|
+
lifecycle.cooldownMs) {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
async auditTriggerLifecycle(definition, kind, details) {
|
|
604
|
+
const timestamp = this.now().toISOString();
|
|
605
|
+
await this.store.appendAudit({
|
|
606
|
+
auditId: `audit_${(await sha256Hex(`${definition.triggerId}:${definition.version}:${kind}:${timestamp}`)).slice(0, 24)}`,
|
|
607
|
+
traceId: `trigger:${definition.triggerId}`,
|
|
608
|
+
timestamp,
|
|
609
|
+
kind,
|
|
610
|
+
entityType: 'trigger',
|
|
611
|
+
entityId: definition.triggerId,
|
|
612
|
+
details: {
|
|
613
|
+
triggerVersion: definition.version,
|
|
614
|
+
...details,
|
|
615
|
+
},
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
async newMatch(definition, key, event) {
|
|
619
|
+
const openedAt = event.occurredAt;
|
|
620
|
+
const expiresAt = new Date(Date.parse(openedAt) + definition.withinMs).toISOString();
|
|
621
|
+
const matchId = `tm_${(await sha256Hex([
|
|
622
|
+
definition.triggerId,
|
|
623
|
+
definition.version,
|
|
624
|
+
key ?? '-',
|
|
625
|
+
event.sourceEventId,
|
|
626
|
+
].join(':'))).slice(0, 24)}`;
|
|
627
|
+
return TriggerMatchRecordSchema.parse({
|
|
628
|
+
protocolVersion: COMPOSITE_TRIGGER_PROTOCOL_VERSION,
|
|
629
|
+
schemaVersion: COMPOSITE_TRIGGER_SCHEMA_VERSION,
|
|
630
|
+
matchId,
|
|
631
|
+
triggerId: definition.triggerId,
|
|
632
|
+
triggerVersion: definition.version,
|
|
633
|
+
status: 'partial',
|
|
634
|
+
correlationKey: key,
|
|
635
|
+
openedAt,
|
|
636
|
+
expiresAt,
|
|
637
|
+
updatedAt: this.now().toISOString(),
|
|
638
|
+
sourceEvents: [],
|
|
639
|
+
correlationDecision: null,
|
|
640
|
+
firedWakeId: null,
|
|
641
|
+
derivedEventIds: [],
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
latestMatch(matchId) {
|
|
645
|
+
return this.store
|
|
646
|
+
.listTriggerMatches()
|
|
647
|
+
.filter((record) => record.matchId === matchId)
|
|
648
|
+
.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0] ?? null;
|
|
649
|
+
}
|
|
650
|
+
async audit(record, kind, details = {}) {
|
|
651
|
+
await this.store.appendAudit({
|
|
652
|
+
auditId: `audit_${(await sha256Hex(`${record.matchId}:${kind}:${record.updatedAt}:${this.store.listTriggerMatches().length}`)).slice(0, 24)}`,
|
|
653
|
+
traceId: record.sourceEvents[0]?.traceId ?? record.matchId,
|
|
654
|
+
timestamp: this.now().toISOString(),
|
|
655
|
+
kind,
|
|
656
|
+
entityType: 'trigger_match',
|
|
657
|
+
entityId: record.matchId,
|
|
658
|
+
details: {
|
|
659
|
+
triggerId: record.triggerId,
|
|
660
|
+
triggerVersion: record.triggerVersion,
|
|
661
|
+
status: record.status,
|
|
662
|
+
correlationKey: record.correlationKey,
|
|
663
|
+
...details,
|
|
664
|
+
},
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { CompositeTriggerDefinition, TemporalCondition, TriggerMatchRecord } from '../intelligenceProtocol/triggerSchemas.js';
|
|
2
|
+
export interface TemporalPendingDeadline {
|
|
3
|
+
conditionId: string;
|
|
4
|
+
dueAt: string;
|
|
5
|
+
}
|
|
6
|
+
export interface TemporalEvaluation {
|
|
7
|
+
outcome: 'satisfied' | 'pending' | 'blocked';
|
|
8
|
+
pendingDeadlines: TemporalPendingDeadline[];
|
|
9
|
+
conditionStates: Array<{
|
|
10
|
+
conditionId: string;
|
|
11
|
+
kind: TemporalCondition['kind'];
|
|
12
|
+
status: 'satisfied' | 'pending' | 'blocked';
|
|
13
|
+
dueAt?: string;
|
|
14
|
+
reason?: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export declare function zonedParts(timestamp: string | Date, timeZone: string): {
|
|
18
|
+
year: number;
|
|
19
|
+
month: number;
|
|
20
|
+
day: number;
|
|
21
|
+
hour: number;
|
|
22
|
+
minute: number;
|
|
23
|
+
second: number;
|
|
24
|
+
weekday: number;
|
|
25
|
+
date: string;
|
|
26
|
+
};
|
|
27
|
+
export declare function localDateTimeToUtc(date: {
|
|
28
|
+
year: number;
|
|
29
|
+
month: number;
|
|
30
|
+
day: number;
|
|
31
|
+
}, clock: string, timeZone: string): Date;
|
|
32
|
+
export declare function evaluateTemporalConditions(definition: CompositeTriggerDefinition, record: TriggerMatchRecord, now: Date): TemporalEvaluation;
|