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.
- package/CHANGELOG.md +100 -0
- package/README.md +107 -26
- package/SECURITY.md +4 -4
- package/bin/mcp-event-intelligence.mjs +3 -3
- package/dist/src/composite/engine.d.ts +14 -4
- package/dist/src/composite/engine.js +533 -208
- package/dist/src/composite/matching.d.ts +1 -5
- package/dist/src/composite/matching.js +45 -54
- package/dist/src/composite/runtimeSupport.js +10 -6
- package/dist/src/composite/store.d.ts +38 -4
- package/dist/src/composite/temporal.d.ts +0 -17
- package/dist/src/composite/temporal.js +0 -266
- package/dist/src/intelligenceProtocol/activationSchemas.d.ts +38 -3
- package/dist/src/intelligenceProtocol/activationSchemas.js +5 -3
- package/dist/src/intelligenceProtocol/index.d.ts +12 -9
- package/dist/src/intelligenceProtocol/index.js +6 -5
- package/dist/src/intelligenceProtocol/lifecycle.js +2 -10
- package/dist/src/intelligenceProtocol/operatorRegistry.d.ts +300 -0
- package/dist/src/intelligenceProtocol/operatorRegistry.js +338 -0
- package/dist/src/intelligenceProtocol/patternV2Schemas.d.ts +188 -0
- package/dist/src/intelligenceProtocol/patternV2Schemas.js +801 -0
- package/dist/src/intelligenceProtocol/schemas.d.ts +4 -189
- package/dist/src/intelligenceProtocol/schemas.js +3 -97
- package/dist/src/intelligenceProtocol/triggerPlanSchemas.d.ts +114 -38
- package/dist/src/intelligenceProtocol/triggerPlanSchemas.js +27 -18
- package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +268 -213
- package/dist/src/intelligenceProtocol/triggerSchemas.js +155 -169
- package/dist/src/intelligenceProtocol/version.d.ts +2 -2
- package/dist/src/intelligenceProtocol/version.js +2 -2
- package/dist/src/mcpEvents/consumer.d.ts +1 -0
- package/dist/src/mcpEvents/consumer.js +1 -0
- package/dist/src/patternV2/evaluator.d.ts +51 -0
- package/dist/src/patternV2/evaluator.js +986 -0
- package/dist/src/semantic/types.d.ts +6 -0
- package/dist/src/semantic/typesafeJevEvaluator.d.ts +1 -0
- package/dist/src/semantic/typesafeJevEvaluator.js +6 -0
- package/package.json +9 -3
- package/scripts/host-integration.d.mts +54 -4
- package/scripts/host-integration.mjs +25 -4
- package/scripts/lib/activation-hydrator.mjs +13 -12
- package/scripts/lib/composite-event-consumer.mjs +10 -28
- package/scripts/lib/composite-wake-coordinator.mjs +19 -19
- package/scripts/lib/derived-contract-registry.mjs +4 -4
- package/scripts/lib/derived-event-coordinator.mjs +69 -6
- package/scripts/lib/local-event-intelligence-runtime.mjs +35 -3
- package/scripts/lib/mcp-events-client.mjs +97 -55
- package/scripts/lib/mcp-events-compatibility.mjs +54 -0
- package/scripts/lib/persistent-event-store.mjs +228 -61
- package/scripts/lib/store-capabilities.mjs +88 -0
- package/scripts/lib/store-conformance.mjs +127 -0
- package/scripts/lib/temporal-deadline-scheduler.mjs +3 -3
- package/scripts/lib/trigger-control-plane.mjs +202 -53
- package/scripts/lib/trigger-inspector.mjs +50 -147
- package/scripts/lib/trigger-planner.mjs +141 -52
- package/scripts/lib/wake-retry-scheduler.mjs +2 -19
- package/scripts/mcp-stdio-server.mjs +46 -61
- package/scripts/service.mjs +19 -138
- package/server.json +4 -4
- package/dist/src/intelligenceProtocol/invariants.d.ts +0 -3
- package/dist/src/intelligenceProtocol/invariants.js +0 -36
- package/dist/src/intelligenceProtocol/mapper.d.ts +0 -14
- package/dist/src/intelligenceProtocol/mapper.js +0 -28
- package/scripts/lib/event-processor.mjs +0 -624
- package/scripts/lib/http-wake-deliverer.mjs +0 -31
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
appendFile,
|
|
3
|
+
mkdir,
|
|
4
|
+
readFile,
|
|
5
|
+
readdir,
|
|
6
|
+
rename,
|
|
7
|
+
writeFile,
|
|
8
|
+
} from 'node:fs/promises';
|
|
2
9
|
import path from 'node:path';
|
|
10
|
+
import {
|
|
11
|
+
REFERENCE_STORE_CAPABILITIES,
|
|
12
|
+
} from './store-capabilities.mjs';
|
|
3
13
|
import {
|
|
4
14
|
AuditChain,
|
|
5
15
|
parseAuditRecord,
|
|
@@ -61,11 +71,17 @@ async function readJsonLines(filePath) {
|
|
|
61
71
|
}
|
|
62
72
|
}
|
|
63
73
|
|
|
74
|
+
async function writeJsonLinesAtomic(filePath, records) {
|
|
75
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
76
|
+
const content = records.length
|
|
77
|
+
? `${records.map((record) => JSON.stringify(record)).join('\n')}\n`
|
|
78
|
+
: '';
|
|
79
|
+
await writeFile(tempPath, content, 'utf8');
|
|
80
|
+
await rename(tempPath, filePath);
|
|
81
|
+
}
|
|
82
|
+
|
|
64
83
|
export class PersistentEventStore {
|
|
65
84
|
#queue = Promise.resolve();
|
|
66
|
-
#eventKeys = new Set();
|
|
67
|
-
#events = [];
|
|
68
|
-
#decisions = [];
|
|
69
85
|
#wakes = [];
|
|
70
86
|
#wakeDeliveries = new Map();
|
|
71
87
|
#triggers = new Map();
|
|
@@ -79,14 +95,14 @@ export class PersistentEventStore {
|
|
|
79
95
|
#temporalDeadlines = new Map();
|
|
80
96
|
#derivedEvents = new Map();
|
|
81
97
|
#derivedContracts = new Map();
|
|
98
|
+
#semanticDecisionCache = new Map();
|
|
99
|
+
#partitionLeases = new Map();
|
|
82
100
|
#auditChain = new AuditChain();
|
|
83
101
|
#scopeStores = new Map();
|
|
84
102
|
|
|
85
103
|
constructor(dataDir) {
|
|
86
104
|
this.dataDir = dataDir;
|
|
87
105
|
this.files = {
|
|
88
|
-
events: path.join(dataDir, 'events.jsonl'),
|
|
89
|
-
decisions: path.join(dataDir, 'decisions.jsonl'),
|
|
90
106
|
wakes: path.join(dataDir, 'wakes.jsonl'),
|
|
91
107
|
wakeDeliveries: path.join(dataDir, 'wake-deliveries.jsonl'),
|
|
92
108
|
triggers: path.join(dataDir, 'triggers.jsonl'),
|
|
@@ -98,10 +114,16 @@ export class PersistentEventStore {
|
|
|
98
114
|
temporalDeadlines: path.join(dataDir, 'temporal-deadlines.jsonl'),
|
|
99
115
|
derivedEvents: path.join(dataDir, 'derived-events.jsonl'),
|
|
100
116
|
derivedContracts: path.join(dataDir, 'derived-contracts.jsonl'),
|
|
117
|
+
semanticDecisionCache: path.join(dataDir, 'semantic-decision-cache.jsonl'),
|
|
118
|
+
partitionLeases: path.join(dataDir, 'partition-leases.jsonl'),
|
|
101
119
|
audit: path.join(dataDir, 'audit.jsonl'),
|
|
102
120
|
};
|
|
103
121
|
}
|
|
104
122
|
|
|
123
|
+
storeCapabilities() {
|
|
124
|
+
return { ...REFERENCE_STORE_CAPABILITIES };
|
|
125
|
+
}
|
|
126
|
+
|
|
105
127
|
async forScope(scopeIdInput = DEFAULT_EVENT_SCOPE_ID) {
|
|
106
128
|
const scopeId = normalizeEventScopeId(scopeIdInput);
|
|
107
129
|
if (scopeId === DEFAULT_EVENT_SCOPE_ID) return this;
|
|
@@ -141,8 +163,6 @@ export class PersistentEventStore {
|
|
|
141
163
|
await mkdir(this.dataDir, { recursive: true });
|
|
142
164
|
|
|
143
165
|
const [
|
|
144
|
-
events,
|
|
145
|
-
decisions,
|
|
146
166
|
wakes,
|
|
147
167
|
wakeDeliveries,
|
|
148
168
|
triggers,
|
|
@@ -154,10 +174,10 @@ export class PersistentEventStore {
|
|
|
154
174
|
temporalDeadlines,
|
|
155
175
|
derivedEvents,
|
|
156
176
|
derivedContracts,
|
|
177
|
+
semanticDecisionCache,
|
|
178
|
+
partitionLeases,
|
|
157
179
|
audit,
|
|
158
180
|
] = await Promise.all([
|
|
159
|
-
readJsonLines(this.files.events),
|
|
160
|
-
readJsonLines(this.files.decisions),
|
|
161
181
|
readJsonLines(this.files.wakes),
|
|
162
182
|
readJsonLines(this.files.wakeDeliveries),
|
|
163
183
|
readJsonLines(this.files.triggers),
|
|
@@ -169,16 +189,16 @@ export class PersistentEventStore {
|
|
|
169
189
|
readJsonLines(this.files.temporalDeadlines),
|
|
170
190
|
readJsonLines(this.files.derivedEvents),
|
|
171
191
|
readJsonLines(this.files.derivedContracts),
|
|
192
|
+
readJsonLines(this.files.semanticDecisionCache),
|
|
193
|
+
readJsonLines(this.files.partitionLeases),
|
|
172
194
|
readJsonLines(this.files.audit),
|
|
173
195
|
]);
|
|
174
196
|
|
|
175
|
-
this.#events = events;
|
|
176
|
-
this.#decisions = decisions;
|
|
177
197
|
this.#wakes = wakes;
|
|
178
198
|
|
|
179
199
|
this.#wakeDeliveries = new Map();
|
|
180
200
|
for (const raw of wakeDeliveries) {
|
|
181
|
-
if (!raw?.wakeId || !raw?.runtime ||
|
|
201
|
+
if (!raw?.wakeId || !raw?.runtime || !raw?.matchId) continue;
|
|
182
202
|
const status = [
|
|
183
203
|
'pending',
|
|
184
204
|
'claimed',
|
|
@@ -190,8 +210,6 @@ export class PersistentEventStore {
|
|
|
190
210
|
: 'pending';
|
|
191
211
|
const record = {
|
|
192
212
|
wakeId: String(raw.wakeId),
|
|
193
|
-
sourceType: raw.sourceType === 'event' ? 'event' : 'composite',
|
|
194
|
-
sourceId: String(raw.sourceId || raw.matchId),
|
|
195
213
|
matchId: raw.matchId ? String(raw.matchId) : '',
|
|
196
214
|
triggerId: String(raw.triggerId || ''),
|
|
197
215
|
triggerVersion: String(raw.triggerVersion || '1'),
|
|
@@ -379,19 +397,48 @@ export class PersistentEventStore {
|
|
|
379
397
|
);
|
|
380
398
|
}
|
|
381
399
|
|
|
382
|
-
this.#
|
|
400
|
+
this.#partitionLeases = new Map();
|
|
401
|
+
for (const raw of partitionLeases) {
|
|
402
|
+
if (!raw?.partitionKey) continue;
|
|
403
|
+
const key = String(raw.partitionKey);
|
|
404
|
+
if (raw.status === 'released') {
|
|
405
|
+
this.#partitionLeases.delete(key);
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (!raw.workerId || !raw.leaseUntil) continue;
|
|
409
|
+
this.#partitionLeases.set(key, {
|
|
410
|
+
partitionKey: key,
|
|
411
|
+
workerId: String(raw.workerId),
|
|
412
|
+
leaseUntil: String(raw.leaseUntil),
|
|
413
|
+
updatedAt: String(raw.updatedAt || new Date(0).toISOString()),
|
|
414
|
+
status: 'active',
|
|
415
|
+
});
|
|
416
|
+
}
|
|
383
417
|
|
|
384
|
-
|
|
385
|
-
|
|
418
|
+
this.#semanticDecisionCache = new Map();
|
|
419
|
+
for (const raw of semanticDecisionCache) {
|
|
420
|
+
if (!raw?.key || !raw?.decision || typeof raw.decision !== 'object') {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
this.#semanticDecisionCache.set(String(raw.key), {
|
|
424
|
+
key: String(raw.key),
|
|
425
|
+
decision: raw.decision,
|
|
426
|
+
createdAt: String(
|
|
427
|
+
raw.createdAt || raw.updatedAt || new Date(0).toISOString()
|
|
428
|
+
),
|
|
429
|
+
updatedAt: String(
|
|
430
|
+
raw.updatedAt || raw.createdAt || new Date(0).toISOString()
|
|
431
|
+
),
|
|
432
|
+
});
|
|
386
433
|
}
|
|
387
434
|
|
|
435
|
+
this.#auditChain = new AuditChain(audit.map(parseAuditRecord));
|
|
436
|
+
|
|
388
437
|
if (!(await this.#auditChain.verify())) {
|
|
389
438
|
throw new Error('Persistent audit chain verification failed');
|
|
390
439
|
}
|
|
391
440
|
|
|
392
441
|
return {
|
|
393
|
-
events: events.length,
|
|
394
|
-
decisions: decisions.length,
|
|
395
442
|
wakes: wakes.length,
|
|
396
443
|
wakeDeliveries: this.#wakeDeliveries.size,
|
|
397
444
|
triggers: this.#triggers.size,
|
|
@@ -404,30 +451,12 @@ export class PersistentEventStore {
|
|
|
404
451
|
temporalDeadlines: this.#temporalDeadlines.size,
|
|
405
452
|
derivedEvents: this.#derivedEvents.size,
|
|
406
453
|
derivedContracts: this.#derivedContracts.size,
|
|
454
|
+
semanticDecisionCache: this.#semanticDecisionCache.size,
|
|
455
|
+
partitionLeases: this.#partitionLeases.size,
|
|
407
456
|
audit: audit.length,
|
|
408
457
|
};
|
|
409
458
|
}
|
|
410
459
|
|
|
411
|
-
async registerEvent(record) {
|
|
412
|
-
return this.#serialized(async () => {
|
|
413
|
-
const key = this.#eventKey(record.lineage);
|
|
414
|
-
if (this.#eventKeys.has(key)) return false;
|
|
415
|
-
|
|
416
|
-
this.#eventKeys.add(key);
|
|
417
|
-
this.#events.push(record);
|
|
418
|
-
await appendFile(this.files.events, `${JSON.stringify(record)}\n`, 'utf8');
|
|
419
|
-
return true;
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
async appendDecision(record) {
|
|
424
|
-
return this.#serialized(async () => {
|
|
425
|
-
this.#decisions.push(record);
|
|
426
|
-
await appendFile(this.files.decisions, `${JSON.stringify(record)}\n`, 'utf8');
|
|
427
|
-
return record;
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
|
|
431
460
|
async appendWake(record) {
|
|
432
461
|
return this.#serialized(async () => {
|
|
433
462
|
this.#wakes.push(record);
|
|
@@ -452,12 +481,7 @@ export class PersistentEventStore {
|
|
|
452
481
|
}
|
|
453
482
|
if (record.status === 'delivered') {
|
|
454
483
|
const latestWake = this.latestWake(record.wakeId);
|
|
455
|
-
const wakeFinalized =
|
|
456
|
-
latestWake?.status === 'delivered' ||
|
|
457
|
-
latestWake?.status === 'handled';
|
|
458
|
-
if (record.sourceType === 'event') {
|
|
459
|
-
return !wakeFinalized;
|
|
460
|
-
}
|
|
484
|
+
const wakeFinalized = latestWake?.status === 'delivered';
|
|
461
485
|
const match = this.#triggerMatches.get(record.matchId);
|
|
462
486
|
return !wakeFinalized || match?.status !== 'fired';
|
|
463
487
|
}
|
|
@@ -472,15 +496,10 @@ export class PersistentEventStore {
|
|
|
472
496
|
async ensureWakeDelivery(input) {
|
|
473
497
|
return this.#serialized(async () => {
|
|
474
498
|
const wakeId = String(input.wakeId || '');
|
|
475
|
-
const sourceType = input.sourceType === 'event' ? 'event' : 'composite';
|
|
476
499
|
const matchId = String(input.matchId || '');
|
|
477
|
-
const sourceId = String(input.sourceId || matchId || '');
|
|
478
500
|
const runtime = String(input.runtime || '');
|
|
479
|
-
if (!wakeId || !
|
|
480
|
-
throw new Error('Wake delivery requires wakeId,
|
|
481
|
-
}
|
|
482
|
-
if (sourceType === 'composite' && !matchId) {
|
|
483
|
-
throw new Error('Composite wake delivery requires matchId');
|
|
501
|
+
if (!wakeId || !matchId || !runtime) {
|
|
502
|
+
throw new Error('Wake delivery requires wakeId, matchId and runtime');
|
|
484
503
|
}
|
|
485
504
|
const existing = this.#wakeDeliveries.get(wakeId);
|
|
486
505
|
if (existing) return existing;
|
|
@@ -488,8 +507,6 @@ export class PersistentEventStore {
|
|
|
488
507
|
const now = String(input.now ?? new Date().toISOString());
|
|
489
508
|
const record = {
|
|
490
509
|
wakeId,
|
|
491
|
-
sourceType,
|
|
492
|
-
sourceId,
|
|
493
510
|
matchId,
|
|
494
511
|
triggerId: String(input.triggerId || ''),
|
|
495
512
|
triggerVersion: String(input.triggerVersion || '1'),
|
|
@@ -1077,6 +1094,162 @@ export class PersistentEventStore {
|
|
|
1077
1094
|
});
|
|
1078
1095
|
}
|
|
1079
1096
|
|
|
1097
|
+
getPartitionLease(partitionKeyInput) {
|
|
1098
|
+
return this.#partitionLeases.get(String(partitionKeyInput || '')) ?? null;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
async claimPartitionLease(partitionKeyInput, {
|
|
1102
|
+
workerId,
|
|
1103
|
+
now = new Date().toISOString(),
|
|
1104
|
+
leaseMs = 30000,
|
|
1105
|
+
} = {}) {
|
|
1106
|
+
return this.#serialized(async () => {
|
|
1107
|
+
const partitionKey = String(partitionKeyInput || '').trim();
|
|
1108
|
+
const owner = String(workerId || '').trim();
|
|
1109
|
+
if (!partitionKey || !owner) {
|
|
1110
|
+
throw new Error('Partition lease requires partitionKey and workerId');
|
|
1111
|
+
}
|
|
1112
|
+
const nowMs = Date.parse(now);
|
|
1113
|
+
const current = this.#partitionLeases.get(partitionKey);
|
|
1114
|
+
if (
|
|
1115
|
+
current &&
|
|
1116
|
+
current.workerId !== owner &&
|
|
1117
|
+
Date.parse(current.leaseUntil) > nowMs
|
|
1118
|
+
) {
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
const record = {
|
|
1122
|
+
partitionKey,
|
|
1123
|
+
workerId: owner,
|
|
1124
|
+
leaseUntil: new Date(
|
|
1125
|
+
nowMs + Math.max(1000, Number(leaseMs) || 30000),
|
|
1126
|
+
).toISOString(),
|
|
1127
|
+
updatedAt: now,
|
|
1128
|
+
status: 'active',
|
|
1129
|
+
};
|
|
1130
|
+
this.#partitionLeases.set(partitionKey, record);
|
|
1131
|
+
await appendFile(
|
|
1132
|
+
this.files.partitionLeases,
|
|
1133
|
+
`${JSON.stringify(record)}\n`,
|
|
1134
|
+
'utf8',
|
|
1135
|
+
);
|
|
1136
|
+
return record;
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
async renewPartitionLease(partitionKeyInput, {
|
|
1141
|
+
workerId,
|
|
1142
|
+
now = new Date().toISOString(),
|
|
1143
|
+
leaseMs = 30000,
|
|
1144
|
+
} = {}) {
|
|
1145
|
+
return this.#serialized(async () => {
|
|
1146
|
+
const partitionKey = String(partitionKeyInput || '').trim();
|
|
1147
|
+
const owner = String(workerId || '').trim();
|
|
1148
|
+
const current = this.#partitionLeases.get(partitionKey);
|
|
1149
|
+
if (!current || current.workerId !== owner) return null;
|
|
1150
|
+
const record = {
|
|
1151
|
+
...current,
|
|
1152
|
+
leaseUntil: new Date(
|
|
1153
|
+
Date.parse(now) + Math.max(1000, Number(leaseMs) || 30000),
|
|
1154
|
+
).toISOString(),
|
|
1155
|
+
updatedAt: now,
|
|
1156
|
+
};
|
|
1157
|
+
this.#partitionLeases.set(partitionKey, record);
|
|
1158
|
+
await appendFile(
|
|
1159
|
+
this.files.partitionLeases,
|
|
1160
|
+
`${JSON.stringify(record)}\n`,
|
|
1161
|
+
'utf8',
|
|
1162
|
+
);
|
|
1163
|
+
return record;
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
async releasePartitionLease(partitionKeyInput, {
|
|
1168
|
+
workerId,
|
|
1169
|
+
now = new Date().toISOString(),
|
|
1170
|
+
} = {}) {
|
|
1171
|
+
return this.#serialized(async () => {
|
|
1172
|
+
const partitionKey = String(partitionKeyInput || '').trim();
|
|
1173
|
+
const owner = String(workerId || '').trim();
|
|
1174
|
+
const current = this.#partitionLeases.get(partitionKey);
|
|
1175
|
+
if (!current || current.workerId !== owner) return false;
|
|
1176
|
+
this.#partitionLeases.delete(partitionKey);
|
|
1177
|
+
await appendFile(
|
|
1178
|
+
this.files.partitionLeases,
|
|
1179
|
+
`${JSON.stringify({
|
|
1180
|
+
partitionKey,
|
|
1181
|
+
workerId: owner,
|
|
1182
|
+
status: 'released',
|
|
1183
|
+
updatedAt: now,
|
|
1184
|
+
})}\n`,
|
|
1185
|
+
'utf8',
|
|
1186
|
+
);
|
|
1187
|
+
return true;
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
async compactMutableState() {
|
|
1192
|
+
return this.#serialized(async () => {
|
|
1193
|
+
const snapshots = [
|
|
1194
|
+
[this.files.wakeDeliveries, [...this.#wakeDeliveries.values()]],
|
|
1195
|
+
[this.files.triggerStates, [...this.#triggerStates.values()]],
|
|
1196
|
+
[this.files.eventSources, [...this.#eventSources.values()]],
|
|
1197
|
+
[this.files.mcpClientStates, [...this.#mcpClientStates.values()]],
|
|
1198
|
+
[this.files.temporalDeadlines, [...this.#temporalDeadlines.values()]],
|
|
1199
|
+
[this.files.derivedContracts, [...this.#derivedContracts.values()]],
|
|
1200
|
+
[this.files.semanticDecisionCache, [...this.#semanticDecisionCache.values()]],
|
|
1201
|
+
[this.files.partitionLeases, [...this.#partitionLeases.values()]],
|
|
1202
|
+
];
|
|
1203
|
+
for (const [filePath, records] of snapshots) {
|
|
1204
|
+
await writeJsonLinesAtomic(filePath, records);
|
|
1205
|
+
}
|
|
1206
|
+
return {
|
|
1207
|
+
ok: true,
|
|
1208
|
+
compacted: snapshots.map(([filePath, records]) => ({
|
|
1209
|
+
file: path.basename(filePath),
|
|
1210
|
+
records: records.length,
|
|
1211
|
+
})),
|
|
1212
|
+
};
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
getSemanticDecisionCache(keyInput) {
|
|
1217
|
+
const record = this.#semanticDecisionCache.get(
|
|
1218
|
+
String(keyInput || '')
|
|
1219
|
+
);
|
|
1220
|
+
return record?.decision ?? null;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
async putSemanticDecisionCache(keyInput, decision) {
|
|
1224
|
+
return this.#serialized(async () => {
|
|
1225
|
+
const key = String(keyInput || '').trim();
|
|
1226
|
+
if (!key || !decision || typeof decision !== 'object') {
|
|
1227
|
+
throw new Error(
|
|
1228
|
+
'Semantic decision cache requires key and decision'
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
const previous = this.#semanticDecisionCache.get(key);
|
|
1232
|
+
const now = new Date().toISOString();
|
|
1233
|
+
const record = {
|
|
1234
|
+
key,
|
|
1235
|
+
decision,
|
|
1236
|
+
createdAt: previous?.createdAt ?? now,
|
|
1237
|
+
updatedAt: now,
|
|
1238
|
+
};
|
|
1239
|
+
this.#semanticDecisionCache.set(key, record);
|
|
1240
|
+
await appendFile(
|
|
1241
|
+
this.files.semanticDecisionCache,
|
|
1242
|
+
`${JSON.stringify(record)}\n`,
|
|
1243
|
+
'utf8',
|
|
1244
|
+
);
|
|
1245
|
+
return record;
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
semanticDecisionCacheSize() {
|
|
1250
|
+
return this.#semanticDecisionCache.size;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1080
1253
|
async appendAudit(input) {
|
|
1081
1254
|
return this.#serialized(async () => {
|
|
1082
1255
|
const record = await this.#auditChain.append(input);
|
|
@@ -1094,8 +1267,6 @@ export class PersistentEventStore {
|
|
|
1094
1267
|
|
|
1095
1268
|
trace(traceId) {
|
|
1096
1269
|
return {
|
|
1097
|
-
events: this.#events.filter((record) => record.lineage?.traceId === traceId),
|
|
1098
|
-
decisions: this.#decisions.filter((record) => record.traceId === traceId),
|
|
1099
1270
|
wakes: this.#wakes.filter((record) => record.traceId === traceId),
|
|
1100
1271
|
triggerMatches: this.listTriggerMatches().filter((record) =>
|
|
1101
1272
|
record.sourceEvents.some((event) => event.traceId === traceId),
|
|
@@ -1128,10 +1299,6 @@ export class PersistentEventStore {
|
|
|
1128
1299
|
await this.drain();
|
|
1129
1300
|
}
|
|
1130
1301
|
|
|
1131
|
-
#eventKey(lineage) {
|
|
1132
|
-
return `${lineage.environmentId}:${lineage.subscriptionId}:${lineage.sourceEventId}`;
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
1302
|
#triggerKey(definition) {
|
|
1136
1303
|
return `${definition.triggerId}@${definition.version}`;
|
|
1137
1304
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export const EVENT_INTELLIGENCE_STORE_CAPABILITIES_VERSION = '1';
|
|
2
|
+
|
|
3
|
+
export const UNDECLARED_STORE_CAPABILITIES = Object.freeze({
|
|
4
|
+
version: EVENT_INTELLIGENCE_STORE_CAPABILITIES_VERSION,
|
|
5
|
+
sharedState: 'none',
|
|
6
|
+
scopeIsolation: 'unknown',
|
|
7
|
+
wakeClaims: 'unknown',
|
|
8
|
+
partitionLeases: 'none',
|
|
9
|
+
mutableCompaction: 'none',
|
|
10
|
+
readModel: 'undeclared',
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const REFERENCE_STORE_CAPABILITIES = Object.freeze({
|
|
14
|
+
version: EVENT_INTELLIGENCE_STORE_CAPABILITIES_VERSION,
|
|
15
|
+
sharedState: 'single-process',
|
|
16
|
+
scopeIsolation: 'strong',
|
|
17
|
+
wakeClaims: 'process-atomic',
|
|
18
|
+
partitionLeases: 'process-atomic',
|
|
19
|
+
mutableCompaction: 'atomic-snapshot',
|
|
20
|
+
readModel: 'synchronous-materialized',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export function describeStoreCapabilities(store) {
|
|
24
|
+
if (
|
|
25
|
+
store &&
|
|
26
|
+
typeof store.storeCapabilities === 'function'
|
|
27
|
+
) {
|
|
28
|
+
return {
|
|
29
|
+
...UNDECLARED_STORE_CAPABILITIES,
|
|
30
|
+
...store.storeCapabilities(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return { ...UNDECLARED_STORE_CAPABILITIES };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function validateSharedStoreCapabilities(store) {
|
|
37
|
+
const capabilities = describeStoreCapabilities(store);
|
|
38
|
+
const errors = [];
|
|
39
|
+
|
|
40
|
+
if (capabilities.sharedState !== 'strong') {
|
|
41
|
+
errors.push(
|
|
42
|
+
`sharedState must be strong; received ${capabilities.sharedState}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (capabilities.scopeIsolation !== 'strong') {
|
|
46
|
+
errors.push(
|
|
47
|
+
`scopeIsolation must be strong; received ${capabilities.scopeIsolation}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (capabilities.wakeClaims !== 'distributed-atomic') {
|
|
51
|
+
errors.push(
|
|
52
|
+
`wakeClaims must be distributed-atomic; received ${capabilities.wakeClaims}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (capabilities.partitionLeases !== 'distributed-atomic') {
|
|
56
|
+
errors.push(
|
|
57
|
+
`partitionLeases must be distributed-atomic; received ${capabilities.partitionLeases}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
for (const method of [
|
|
61
|
+
'claimPartitionLease',
|
|
62
|
+
'renewPartitionLease',
|
|
63
|
+
'releasePartitionLease',
|
|
64
|
+
]) {
|
|
65
|
+
if (typeof store?.[method] !== 'function') {
|
|
66
|
+
errors.push(`shared store is missing ${method}()`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
ok: errors.length === 0,
|
|
72
|
+
capabilities,
|
|
73
|
+
errors,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function assertSharedStoreCapabilities(store) {
|
|
78
|
+
const result = validateSharedStoreCapabilities(store);
|
|
79
|
+
if (result.ok) return result.capabilities;
|
|
80
|
+
|
|
81
|
+
const error = new Error(
|
|
82
|
+
'Event Intelligence shared-store requirements failed: ' +
|
|
83
|
+
result.errors.join('; '),
|
|
84
|
+
);
|
|
85
|
+
error.code = 'EVENT_INTELLIGENCE_SHARED_STORE_REQUIRED';
|
|
86
|
+
error.details = result;
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import {
|
|
3
|
+
describeStoreCapabilities,
|
|
4
|
+
validateSharedStoreCapabilities,
|
|
5
|
+
} from './store-capabilities.mjs';
|
|
6
|
+
|
|
7
|
+
export async function runStoreConformance({
|
|
8
|
+
createStore,
|
|
9
|
+
expectShared = false,
|
|
10
|
+
} = {}) {
|
|
11
|
+
if (typeof createStore !== 'function') {
|
|
12
|
+
throw new Error('runStoreConformance requires createStore()');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const store = await createStore();
|
|
16
|
+
if (typeof store.init === 'function') await store.init();
|
|
17
|
+
|
|
18
|
+
const report = {
|
|
19
|
+
schema: 'event-intelligence.store-conformance.v1',
|
|
20
|
+
capabilities: describeStoreCapabilities(store),
|
|
21
|
+
checks: [],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const check = async (name, fn) => {
|
|
25
|
+
await fn();
|
|
26
|
+
report.checks.push({ name, ok: true });
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
await check('trigger-state round trip', async () => {
|
|
30
|
+
await store.putTrigger({
|
|
31
|
+
protocolVersion: '0.1.0',
|
|
32
|
+
schemaVersion: 'trigger.v0.1',
|
|
33
|
+
triggerId: 'store-conf-trigger',
|
|
34
|
+
version: '1',
|
|
35
|
+
conditionOnly: true,
|
|
36
|
+
clauses: [{
|
|
37
|
+
id: 'event',
|
|
38
|
+
event: 'store.conformance.event',
|
|
39
|
+
arguments: {},
|
|
40
|
+
where: [],
|
|
41
|
+
}],
|
|
42
|
+
expression: { kind: 'anyOf', refs: ['event'] },
|
|
43
|
+
temporal: [],
|
|
44
|
+
withinMs: 60000,
|
|
45
|
+
lifecycle: {
|
|
46
|
+
oneShot: false,
|
|
47
|
+
cooldownMs: 0,
|
|
48
|
+
completeOnGoal: false,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
await store.setTriggerState(
|
|
52
|
+
'store-conf-trigger',
|
|
53
|
+
'1',
|
|
54
|
+
'paused',
|
|
55
|
+
{ type: 'system', principal_id: 'conformance' },
|
|
56
|
+
);
|
|
57
|
+
assert.equal(
|
|
58
|
+
(await store.getTriggerState('store-conf-trigger', '1')).status,
|
|
59
|
+
'paused',
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await check('wake claim exclusion', async () => {
|
|
64
|
+
await store.ensureWakeDelivery({
|
|
65
|
+
wakeId: 'store-conf-wake',
|
|
66
|
+
sourceType: 'event',
|
|
67
|
+
sourceId: 'source-1',
|
|
68
|
+
runtime: 'conformance',
|
|
69
|
+
now: '2026-09-30T00:00:00.000Z',
|
|
70
|
+
});
|
|
71
|
+
const [a, b] = await Promise.all([
|
|
72
|
+
store.claimWakeDelivery('store-conf-wake', {
|
|
73
|
+
workerId: 'a',
|
|
74
|
+
now: '2026-09-30T00:00:00.000Z',
|
|
75
|
+
leaseMs: 30000,
|
|
76
|
+
}),
|
|
77
|
+
store.claimWakeDelivery('store-conf-wake', {
|
|
78
|
+
workerId: 'b',
|
|
79
|
+
now: '2026-09-30T00:00:00.000Z',
|
|
80
|
+
leaseMs: 30000,
|
|
81
|
+
}),
|
|
82
|
+
]);
|
|
83
|
+
assert.equal([a, b].filter(Boolean).length, 1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
typeof store.claimPartitionLease === 'function' &&
|
|
88
|
+
typeof store.releasePartitionLease === 'function'
|
|
89
|
+
) {
|
|
90
|
+
await check('partition lease exclusion', async () => {
|
|
91
|
+
const [a, b] = await Promise.all([
|
|
92
|
+
store.claimPartitionLease('partition-1', {
|
|
93
|
+
workerId: 'a',
|
|
94
|
+
now: '2026-09-30T00:00:00.000Z',
|
|
95
|
+
leaseMs: 30000,
|
|
96
|
+
}),
|
|
97
|
+
store.claimPartitionLease('partition-1', {
|
|
98
|
+
workerId: 'b',
|
|
99
|
+
now: '2026-09-30T00:00:00.000Z',
|
|
100
|
+
leaseMs: 30000,
|
|
101
|
+
}),
|
|
102
|
+
]);
|
|
103
|
+
assert.equal([a, b].filter(Boolean).length, 1);
|
|
104
|
+
const owner = a ? 'a' : 'b';
|
|
105
|
+
await store.releasePartitionLease('partition-1', {
|
|
106
|
+
workerId: owner,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (typeof store.compactMutableState === 'function') {
|
|
112
|
+
await check('mutable-state compaction', async () => {
|
|
113
|
+
const result = await store.compactMutableState();
|
|
114
|
+
assert.equal(result?.ok, true);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const shared = validateSharedStoreCapabilities(store);
|
|
119
|
+
if (expectShared) {
|
|
120
|
+
assert.equal(shared.ok, true, shared.errors.join('; '));
|
|
121
|
+
}
|
|
122
|
+
report.sharedReady = shared.ok;
|
|
123
|
+
report.sharedErrors = shared.errors;
|
|
124
|
+
|
|
125
|
+
if (typeof store.close === 'function') await store.close();
|
|
126
|
+
return report;
|
|
127
|
+
}
|
|
@@ -19,14 +19,14 @@ export class TemporalDeadlineScheduler {
|
|
|
19
19
|
|
|
20
20
|
try {
|
|
21
21
|
const now = this.now();
|
|
22
|
-
const due = this.store.listDueTemporalDeadlines(now.toISOString());
|
|
22
|
+
const due = await this.store.listDueTemporalDeadlines(now.toISOString());
|
|
23
23
|
const outcomes = [];
|
|
24
24
|
|
|
25
25
|
for (const deadline of due) {
|
|
26
|
-
const latest = this.store.getTemporalDeadline(deadline.deadlineId);
|
|
26
|
+
const latest = await this.store.getTemporalDeadline(deadline.deadlineId);
|
|
27
27
|
if (!latest || latest.status !== 'pending') continue;
|
|
28
28
|
|
|
29
|
-
const match = this.store.listTriggerMatches(deadline.triggerId)
|
|
29
|
+
const match = (await this.store.listTriggerMatches(deadline.triggerId))
|
|
30
30
|
.filter((record) => record.matchId === deadline.matchId)
|
|
31
31
|
.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0];
|
|
32
32
|
|