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.
Files changed (63) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/LICENSE +201 -0
  3. package/README.md +298 -0
  4. package/SECURITY.md +42 -0
  5. package/bin/mcp-event-intelligence.mjs +52 -0
  6. package/dist/src/composite/engine.d.ts +87 -0
  7. package/dist/src/composite/engine.js +667 -0
  8. package/dist/src/composite/temporal.d.ts +32 -0
  9. package/dist/src/composite/temporal.js +317 -0
  10. package/dist/src/intelligenceProtocol/audit.d.ts +19 -0
  11. package/dist/src/intelligenceProtocol/audit.js +56 -0
  12. package/dist/src/intelligenceProtocol/canonical.d.ts +2 -0
  13. package/dist/src/intelligenceProtocol/canonical.js +21 -0
  14. package/dist/src/intelligenceProtocol/index.d.ts +12 -0
  15. package/dist/src/intelligenceProtocol/index.js +8 -0
  16. package/dist/src/intelligenceProtocol/invariants.d.ts +3 -0
  17. package/dist/src/intelligenceProtocol/invariants.js +36 -0
  18. package/dist/src/intelligenceProtocol/lifecycle.d.ts +4 -0
  19. package/dist/src/intelligenceProtocol/lifecycle.js +24 -0
  20. package/dist/src/intelligenceProtocol/mapper.d.ts +14 -0
  21. package/dist/src/intelligenceProtocol/mapper.js +28 -0
  22. package/dist/src/intelligenceProtocol/schemas.d.ts +325 -0
  23. package/dist/src/intelligenceProtocol/schemas.js +182 -0
  24. package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +488 -0
  25. package/dist/src/intelligenceProtocol/triggerSchemas.js +379 -0
  26. package/dist/src/intelligenceProtocol/version.d.ts +7 -0
  27. package/dist/src/intelligenceProtocol/version.js +7 -0
  28. package/dist/src/mcpEvents/consumer.d.ts +7 -0
  29. package/dist/src/mcpEvents/consumer.js +14 -0
  30. package/dist/src/mcpEvents/server.d.ts +41 -0
  31. package/dist/src/mcpEvents/server.js +122 -0
  32. package/dist/src/protocol/types.d.ts +37 -0
  33. package/dist/src/protocol/types.js +1 -0
  34. package/dist/src/semantic/conditionEngine.d.ts +7 -0
  35. package/dist/src/semantic/conditionEngine.js +65 -0
  36. package/dist/src/semantic/types.d.ts +31 -0
  37. package/dist/src/semantic/types.js +1 -0
  38. package/dist/src/semantic/typesafeJevEvaluator.d.ts +23 -0
  39. package/dist/src/semantic/typesafeJevEvaluator.js +95 -0
  40. package/package.json +85 -0
  41. package/scripts/host-integration.d.mts +86 -0
  42. package/scripts/host-integration.mjs +216 -0
  43. package/scripts/lib/composite-event-consumer.mjs +172 -0
  44. package/scripts/lib/composite-wake-coordinator.mjs +185 -0
  45. package/scripts/lib/derived-contract-registry.mjs +210 -0
  46. package/scripts/lib/derived-event-coordinator.mjs +242 -0
  47. package/scripts/lib/event-processor.mjs +409 -0
  48. package/scripts/lib/generic-runtime-wake.mjs +76 -0
  49. package/scripts/lib/github-mcp-events-adapter.mjs +66 -0
  50. package/scripts/lib/github-webhook.mjs +88 -0
  51. package/scripts/lib/http-wake-deliverer.mjs +31 -0
  52. package/scripts/lib/local-event-intelligence-runtime.mjs +193 -0
  53. package/scripts/lib/mcp-events-client.mjs +519 -0
  54. package/scripts/lib/persistent-event-store.mjs +769 -0
  55. package/scripts/lib/temporal-deadline-scheduler.mjs +111 -0
  56. package/scripts/lib/trigger-control-plane.mjs +939 -0
  57. package/scripts/lib/trigger-inspector.mjs +631 -0
  58. package/scripts/lib/wake-signature.mjs +10 -0
  59. package/scripts/mcp-stdio-server.mjs +380 -0
  60. package/scripts/mcp-stdio.mjs +3 -0
  61. package/scripts/run-tests.mjs +51 -0
  62. package/scripts/service.mjs +756 -0
  63. package/server.json +68 -0
@@ -0,0 +1,242 @@
1
+ import {
2
+ parseDerivedEventRecord,
3
+ sha256Hex,
4
+ } from '../../dist/src/intelligenceProtocol/index.js';
5
+
6
+ export const DERIVED_EVENT_SERVER_ID = 'event-intelligence:derived';
7
+ export const DERIVED_EVENT_CONNECTION_ID = 'event-intelligence:derived';
8
+
9
+ function getByPath(source, path) {
10
+ let current = source;
11
+ for (const part of String(path).split('.')) {
12
+ if (
13
+ current === null ||
14
+ typeof current !== 'object' ||
15
+ Array.isArray(current)
16
+ ) return undefined;
17
+ current = current[part];
18
+ }
19
+ return current;
20
+ }
21
+
22
+ function evidenceRef(source) {
23
+ return {
24
+ serverId: source.serverId ?? null,
25
+ eventName: source.eventName,
26
+ sourceEventId: source.sourceEventId,
27
+ traceId: source.traceId,
28
+ occurredAt: source.occurredAt,
29
+ payloadHash: source.payloadHash ?? null,
30
+ contractVersion:
31
+ source.data?._derived?.contractVersion ?? null,
32
+ schemaFingerprint:
33
+ source.data?._derived?.schemaFingerprint ?? null,
34
+ };
35
+ }
36
+
37
+ function dedupeEvidence(refs) {
38
+ const seen = new Set();
39
+ const result = [];
40
+ for (const ref of refs) {
41
+ const key =
42
+ `${ref.serverId ?? ''}::${ref.eventName}::${ref.sourceEventId}`;
43
+ if (seen.has(key)) continue;
44
+ seen.add(key);
45
+ result.push(ref);
46
+ }
47
+ return result.sort((a, b) =>
48
+ a.occurredAt.localeCompare(b.occurredAt) ||
49
+ a.eventName.localeCompare(b.eventName) ||
50
+ a.sourceEventId.localeCompare(b.sourceEventId)
51
+ );
52
+ }
53
+
54
+ function latestForClause(match, clauseId) {
55
+ return match.sourceEvents
56
+ .filter((event) => event.clauseId === clauseId)
57
+ .sort((a, b) =>
58
+ Date.parse(b.occurredAt) - Date.parse(a.occurredAt) ||
59
+ b.sourceEventId.localeCompare(a.sourceEventId)
60
+ )[0] ?? null;
61
+ }
62
+
63
+ export class DerivedEventCoordinator {
64
+ constructor({
65
+ store,
66
+ now = () => new Date(),
67
+ }) {
68
+ this.store = store;
69
+ this.now = now;
70
+ }
71
+
72
+ async emitMatched(match, definition) {
73
+ const spec = definition?.derivedEvent;
74
+ if (!spec || !match || match.status !== 'matched') {
75
+ return {
76
+ status: 'not_applicable',
77
+ accepted: false,
78
+ record: null,
79
+ };
80
+ }
81
+
82
+ const eventId = `derived_${(
83
+ await sha256Hex({
84
+ triggerId: definition.triggerId,
85
+ triggerVersion: definition.version,
86
+ matchId: match.matchId,
87
+ eventName: spec.name,
88
+ contractVersion: spec.contractVersion,
89
+ })
90
+ ).slice(0, 24)}`;
91
+
92
+ const existing = this.store.getDerivedEvent(
93
+ eventId,
94
+ DERIVED_EVENT_SERVER_ID,
95
+ );
96
+ if (existing) {
97
+ await this.audit(existing, 'derived_event.replayed', {
98
+ reason: 'stable_event_id_already_persisted',
99
+ });
100
+ return {
101
+ status: 'replayed',
102
+ accepted: false,
103
+ record: existing,
104
+ };
105
+ }
106
+
107
+ const directParents = dedupeEvidence(
108
+ match.sourceEvents.map(evidenceRef),
109
+ );
110
+ const rootEvidence = [];
111
+ for (const source of match.sourceEvents) {
112
+ if (source.serverId === DERIVED_EVENT_SERVER_ID) {
113
+ const parent = this.store.getDerivedEvent(
114
+ source.sourceEventId,
115
+ DERIVED_EVENT_SERVER_ID,
116
+ );
117
+ if (parent) {
118
+ rootEvidence.push(...parent.rootEvidence);
119
+ continue;
120
+ }
121
+ }
122
+ rootEvidence.push(evidenceRef(source));
123
+ }
124
+
125
+ const projected = {};
126
+ for (const projection of spec.projections ?? []) {
127
+ const source = latestForClause(match, projection.ref);
128
+ if (!source) {
129
+ throw new Error(
130
+ `Derived event projection source missing: ${projection.ref}`,
131
+ );
132
+ }
133
+ const value = getByPath(source.data, projection.path);
134
+ if (
135
+ value !== undefined &&
136
+ (
137
+ typeof value === 'string' ||
138
+ typeof value === 'number' ||
139
+ typeof value === 'boolean'
140
+ )
141
+ ) {
142
+ projected[projection.key] = value;
143
+ } else if (value !== undefined) {
144
+ throw new Error(
145
+ `Derived event projection must resolve to scalar: ${projection.ref}.${projection.path}`,
146
+ );
147
+ }
148
+ }
149
+
150
+ const contract = this.store.getDerivedContract(
151
+ spec.name,
152
+ spec.contractVersion,
153
+ );
154
+ if (!contract) {
155
+ const error = new Error(
156
+ `Derived contract missing for ${spec.name}@${spec.contractVersion}`,
157
+ );
158
+ error.code = 'DERIVED_CONTRACT_NOT_FOUND';
159
+ throw error;
160
+ }
161
+
162
+ const data = {
163
+ ...(spec.constants ?? {}),
164
+ ...projected,
165
+ _derived: {
166
+ triggerId: definition.triggerId,
167
+ triggerVersion: definition.version,
168
+ matchId: match.matchId,
169
+ correlationKey: match.correlationKey,
170
+ contractVersion: spec.contractVersion,
171
+ schemaFingerprint: contract.schemaFingerprint,
172
+ },
173
+ };
174
+ const payloadHash = await sha256Hex(data);
175
+ const event = {
176
+ traceId: `derived:${eventId}`,
177
+ sourceEventId: eventId,
178
+ name: spec.name,
179
+ occurredAt: match.updatedAt,
180
+ provider: 'event-intelligence',
181
+ serverId: DERIVED_EVENT_SERVER_ID,
182
+ payloadHash,
183
+ data,
184
+ };
185
+ const record = parseDerivedEventRecord({
186
+ event,
187
+ triggerId: definition.triggerId,
188
+ triggerVersion: definition.version,
189
+ matchId: match.matchId,
190
+ directParents,
191
+ rootEvidence: dedupeEvidence(rootEvidence),
192
+ createdAt: this.now().toISOString(),
193
+ });
194
+
195
+ const receipt = await this.store.appendDerivedEvent(record);
196
+ if (!receipt.accepted) {
197
+ await this.audit(receipt.record, 'derived_event.replayed', {
198
+ reason: 'store_dedup',
199
+ });
200
+ return {
201
+ status: 'replayed',
202
+ accepted: false,
203
+ record: receipt.record,
204
+ };
205
+ }
206
+
207
+ await this.audit(record, 'derived_event.created', {
208
+ parentCount: record.directParents.length,
209
+ rootEvidenceCount: record.rootEvidence.length,
210
+ });
211
+
212
+ return {
213
+ status: 'created',
214
+ accepted: true,
215
+ record,
216
+ };
217
+ }
218
+
219
+ async audit(record, kind, details) {
220
+ await this.store.appendAudit({
221
+ auditId: `audit_${(
222
+ await sha256Hex({
223
+ kind,
224
+ sourceEventId: record.event.sourceEventId,
225
+ sequence: this.store.auditLength(),
226
+ })
227
+ ).slice(0, 24)}`,
228
+ traceId: record.event.traceId,
229
+ timestamp: this.now().toISOString(),
230
+ kind,
231
+ entityType: 'derived_event',
232
+ entityId: record.event.sourceEventId,
233
+ details: {
234
+ triggerId: record.triggerId,
235
+ triggerVersion: record.triggerVersion,
236
+ matchId: record.matchId,
237
+ eventName: record.event.name,
238
+ ...details,
239
+ },
240
+ });
241
+ }
242
+ }
@@ -0,0 +1,409 @@
1
+ import {
2
+ assertDecisionLineage,
3
+ assertWakeLineage,
4
+ mapMcpEventToLineage,
5
+ parseEventIngestRequest,
6
+ SemanticDecisionRecordSchema,
7
+ WakeRecordSchema,
8
+ sha256Hex,
9
+ } from '../../dist/src/intelligenceProtocol/index.js';
10
+ import {
11
+ SemanticConditionEngine,
12
+ } from '../../dist/src/semantic/conditionEngine.js';
13
+
14
+ function nowIso(now) {
15
+ return now().toISOString();
16
+ }
17
+
18
+ async function stableId(prefix, value) {
19
+ const hash = await sha256Hex(value);
20
+ return `${prefix}_${hash.slice(0, 24)}`;
21
+ }
22
+
23
+ export class EventProcessor {
24
+ constructor({
25
+ store,
26
+ evaluator = null,
27
+ wakeDeliverer = null,
28
+ now = () => new Date(),
29
+ }) {
30
+ this.store = store;
31
+ this.evaluator = evaluator;
32
+ this.wakeDeliverer = wakeDeliverer;
33
+ this.now = now;
34
+ }
35
+
36
+ async ingest(rawRequest) {
37
+ const request = parseEventIngestRequest(rawRequest);
38
+ const traceId =
39
+ request.context.traceId ??
40
+ (await stableId(
41
+ 'trace',
42
+ [
43
+ request.context.environmentId,
44
+ request.context.subscriptionId,
45
+ request.event.eventId,
46
+ ].join(':'),
47
+ ));
48
+
49
+ const lineage = await mapMcpEventToLineage({
50
+ event: request.event,
51
+ traceId,
52
+ environmentId: request.context.environmentId,
53
+ subscriptionId: request.context.subscriptionId,
54
+ serverId: request.context.serverId,
55
+ transport: request.context.transport,
56
+ provider: request.context.provider,
57
+ target: request.context.target,
58
+ observedAt: nowIso(this.now),
59
+ });
60
+
61
+ const accepted = await this.store.registerEvent({
62
+ lineage,
63
+ event: request.event,
64
+ });
65
+
66
+ if (!accepted) {
67
+ await this.store.appendAudit({
68
+ auditId: await stableId('audit', `${traceId}:duplicate:${this.store.auditLength()}`),
69
+ traceId,
70
+ timestamp: nowIso(this.now),
71
+ kind: 'event.duplicate',
72
+ entityType: 'event',
73
+ entityId: request.event.eventId,
74
+ toState: 'duplicate',
75
+ details: {
76
+ subscriptionId: request.context.subscriptionId,
77
+ },
78
+ });
79
+
80
+ return {
81
+ traceId,
82
+ sourceEventId: request.event.eventId,
83
+ status: 'duplicate',
84
+ wake: null,
85
+ };
86
+ }
87
+
88
+ await this.store.appendAudit({
89
+ auditId: await stableId('audit', `${traceId}:received`),
90
+ traceId,
91
+ timestamp: nowIso(this.now),
92
+ kind: 'event.received',
93
+ entityType: 'event',
94
+ entityId: request.event.eventId,
95
+ toState: 'received',
96
+ details: {
97
+ eventName: request.event.name,
98
+ payloadHash: lineage.event.payloadHash,
99
+ },
100
+ });
101
+
102
+ let state = 'received';
103
+ let decision = null;
104
+
105
+ if (request.semanticCondition) {
106
+ if (!this.evaluator) {
107
+ await this.store.appendAudit({
108
+ auditId: await stableId('audit', `${traceId}:semantic-unavailable`),
109
+ traceId,
110
+ timestamp: nowIso(this.now),
111
+ kind: 'error',
112
+ entityType: 'event',
113
+ entityId: request.event.eventId,
114
+ fromState: state,
115
+ toState: 'failed',
116
+ details: { code: 'SEMANTIC_EVALUATOR_UNAVAILABLE' },
117
+ });
118
+ throw new Error('Semantic condition supplied but no evaluator is configured');
119
+ }
120
+
121
+ await this.store.appendAudit({
122
+ auditId: await stableId('audit', `${traceId}:evaluating`),
123
+ traceId,
124
+ timestamp: nowIso(this.now),
125
+ kind: 'lifecycle.transition',
126
+ entityType: 'event',
127
+ entityId: request.event.eventId,
128
+ fromState: state,
129
+ toState: 'evaluating',
130
+ });
131
+ state = 'evaluating';
132
+
133
+ try {
134
+ const engine = new SemanticConditionEngine(this.evaluator);
135
+ const result = await engine.evaluate(
136
+ { data: request.event.data },
137
+ request.semanticCondition,
138
+ );
139
+
140
+ decision = SemanticDecisionRecordSchema.parse({
141
+ decisionId: await stableId('decision', `${traceId}:semantic`),
142
+ traceId,
143
+ subscriptionId: request.context.subscriptionId,
144
+ sourceEventId: request.event.eventId,
145
+ createdAt: nowIso(this.now),
146
+ evaluator: result.evaluator,
147
+ outcome: result.outcome,
148
+ probability: result.probability,
149
+ matched: result.matched,
150
+ shouldEscalate: result.shouldEscalate,
151
+ policy: {
152
+ matchThreshold: request.semanticCondition.matchThreshold,
153
+ rejectThreshold: request.semanticCondition.rejectThreshold,
154
+ uncertain: request.semanticCondition.uncertain,
155
+ },
156
+ inputFields: request.semanticCondition.input,
157
+ providerEvidence: result.metadata
158
+ ? {
159
+ requestedModel:
160
+ typeof result.metadata.requestedModel === 'string'
161
+ ? result.metadata.requestedModel
162
+ : undefined,
163
+ resolvedModel:
164
+ typeof result.metadata.resolvedModel === 'string'
165
+ ? result.metadata.resolvedModel
166
+ : undefined,
167
+ requestId:
168
+ typeof result.metadata.requestId === 'string'
169
+ ? result.metadata.requestId
170
+ : undefined,
171
+ httpStatus:
172
+ typeof result.metadata.httpStatus === 'number'
173
+ ? result.metadata.httpStatus
174
+ : undefined,
175
+ inputTokens:
176
+ typeof result.metadata.inputTokens === 'number'
177
+ ? result.metadata.inputTokens
178
+ : undefined,
179
+ outputTokens:
180
+ typeof result.metadata.outputTokens === 'number'
181
+ ? result.metadata.outputTokens
182
+ : undefined,
183
+ providerReportedCost:
184
+ typeof result.metadata.cost === 'number'
185
+ ? result.metadata.cost
186
+ : undefined,
187
+ }
188
+ : undefined,
189
+ });
190
+
191
+ assertDecisionLineage(lineage, decision);
192
+ await this.store.appendDecision(decision);
193
+ await this.store.appendAudit({
194
+ auditId: await stableId('audit', `${traceId}:decision`),
195
+ traceId,
196
+ timestamp: nowIso(this.now),
197
+ kind: 'decision.evaluated',
198
+ entityType: 'decision',
199
+ entityId: decision.decisionId,
200
+ details: {
201
+ evaluator: decision.evaluator,
202
+ outcome: decision.outcome,
203
+ probability: decision.probability,
204
+ inputFields: decision.inputFields,
205
+ providerEvidence: decision.providerEvidence ?? null,
206
+ },
207
+ });
208
+
209
+ const nextState =
210
+ result.outcome === 'match'
211
+ ? 'matched'
212
+ : result.outcome === 'reject'
213
+ ? 'rejected'
214
+ : 'escalated';
215
+
216
+ await this.store.appendAudit({
217
+ auditId: await stableId('audit', `${traceId}:${nextState}`),
218
+ traceId,
219
+ timestamp: nowIso(this.now),
220
+ kind: 'lifecycle.transition',
221
+ entityType: 'event',
222
+ entityId: request.event.eventId,
223
+ fromState: state,
224
+ toState: nextState,
225
+ });
226
+ state = nextState;
227
+
228
+ if (state === 'rejected' || (state === 'escalated' && !request.wakeOnEscalation)) {
229
+ return {
230
+ traceId,
231
+ sourceEventId: request.event.eventId,
232
+ status: state,
233
+ decision,
234
+ wake: null,
235
+ };
236
+ }
237
+ } catch (error) {
238
+ if (state === 'evaluating') {
239
+ await this.store.appendAudit({
240
+ auditId: await stableId('audit', `${traceId}:evaluation-failed`),
241
+ traceId,
242
+ timestamp: nowIso(this.now),
243
+ kind: 'error',
244
+ entityType: 'event',
245
+ entityId: request.event.eventId,
246
+ fromState: state,
247
+ toState: 'failed',
248
+ details: {
249
+ message: error instanceof Error ? error.message : 'semantic evaluation failed',
250
+ },
251
+ });
252
+ }
253
+ throw error;
254
+ }
255
+ } else {
256
+ await this.store.appendAudit({
257
+ auditId: await stableId('audit', `${traceId}:direct-match`),
258
+ traceId,
259
+ timestamp: nowIso(this.now),
260
+ kind: 'lifecycle.transition',
261
+ entityType: 'event',
262
+ entityId: request.event.eventId,
263
+ fromState: state,
264
+ toState: 'matched',
265
+ details: { reason: 'no_semantic_condition' },
266
+ });
267
+ state = 'matched';
268
+ }
269
+
270
+ const wakeId = await stableId('wake', `${traceId}:${request.context.target.id}`);
271
+ const queuedWake = WakeRecordSchema.parse({
272
+ wakeId,
273
+ traceId,
274
+ decisionId: decision?.decisionId ?? null,
275
+ subscriptionId: request.context.subscriptionId,
276
+ sourceEventId: request.event.eventId,
277
+ createdAt: nowIso(this.now),
278
+ target: request.context.target,
279
+ status: 'queued',
280
+ });
281
+
282
+ assertWakeLineage(lineage, decision, queuedWake);
283
+ await this.store.appendWake(queuedWake);
284
+ await this.store.appendAudit({
285
+ auditId: await stableId('audit', `${traceId}:wake-queued`),
286
+ traceId,
287
+ timestamp: nowIso(this.now),
288
+ kind: 'wake.queued',
289
+ entityType: 'wake',
290
+ entityId: wakeId,
291
+ fromState: state,
292
+ toState: 'wake_queued',
293
+ details: {
294
+ runtime: queuedWake.target.runtime,
295
+ targetKind: queuedWake.target.kind,
296
+ targetId: queuedWake.target.id,
297
+ },
298
+ });
299
+
300
+ if (!this.wakeDeliverer) {
301
+ return {
302
+ traceId,
303
+ sourceEventId: request.event.eventId,
304
+ status: 'wake_queued',
305
+ decision,
306
+ wake: queuedWake,
307
+ };
308
+ }
309
+
310
+ try {
311
+ const receipt = await this.wakeDeliverer({
312
+ wake: queuedWake,
313
+ lineage,
314
+ event: request.event,
315
+ decision,
316
+ });
317
+
318
+ const deliveredWake = WakeRecordSchema.parse({
319
+ ...queuedWake,
320
+ createdAt: nowIso(this.now),
321
+ status: 'delivered',
322
+ runtimeReceiptId: receipt?.runtimeReceiptId ?? undefined,
323
+ });
324
+ await this.store.appendWake(deliveredWake);
325
+ await this.store.appendAudit({
326
+ auditId: await stableId('audit', `${traceId}:wake-delivered`),
327
+ traceId,
328
+ timestamp: nowIso(this.now),
329
+ kind: 'wake.delivered',
330
+ entityType: 'wake',
331
+ entityId: wakeId,
332
+ fromState: 'wake_queued',
333
+ toState: 'wake_delivered',
334
+ details: {
335
+ runtimeReceiptId: deliveredWake.runtimeReceiptId ?? null,
336
+ },
337
+ });
338
+
339
+ return {
340
+ traceId,
341
+ sourceEventId: request.event.eventId,
342
+ status: 'wake_delivered',
343
+ decision,
344
+ wake: deliveredWake,
345
+ };
346
+ } catch (error) {
347
+ const deadLetter = WakeRecordSchema.parse({
348
+ ...queuedWake,
349
+ createdAt: nowIso(this.now),
350
+ status: 'dead_letter',
351
+ });
352
+ await this.store.appendWake(deadLetter);
353
+ await this.store.appendAudit({
354
+ auditId: await stableId('audit', `${traceId}:wake-dead-letter`),
355
+ traceId,
356
+ timestamp: nowIso(this.now),
357
+ kind: 'wake.dead_letter',
358
+ entityType: 'wake',
359
+ entityId: wakeId,
360
+ fromState: 'wake_queued',
361
+ toState: 'dead_letter',
362
+ details: {
363
+ message: error instanceof Error ? error.message : 'wake delivery failed',
364
+ },
365
+ });
366
+
367
+ return {
368
+ traceId,
369
+ sourceEventId: request.event.eventId,
370
+ status: 'dead_letter',
371
+ decision,
372
+ wake: deadLetter,
373
+ };
374
+ }
375
+ }
376
+
377
+ async acknowledgeWake(wakeId, runtimeReceiptId) {
378
+ const latest = this.store.latestWake(wakeId);
379
+ if (!latest) throw new Error('Unknown wake');
380
+ if (latest.status === 'handled') return latest;
381
+ if (latest.status !== 'delivered') {
382
+ throw new Error(`Wake must be delivered before acknowledgement; current=${latest.status}`);
383
+ }
384
+
385
+ const handled = WakeRecordSchema.parse({
386
+ ...latest,
387
+ createdAt: nowIso(this.now),
388
+ status: 'handled',
389
+ runtimeReceiptId: runtimeReceiptId ?? latest.runtimeReceiptId,
390
+ });
391
+
392
+ await this.store.appendWake(handled);
393
+ await this.store.appendAudit({
394
+ auditId: await stableId('audit', `${latest.traceId}:wake-handled`),
395
+ traceId: latest.traceId,
396
+ timestamp: nowIso(this.now),
397
+ kind: 'wake.handled',
398
+ entityType: 'wake',
399
+ entityId: wakeId,
400
+ fromState: 'wake_delivered',
401
+ toState: 'handled',
402
+ details: {
403
+ runtimeReceiptId: handled.runtimeReceiptId ?? null,
404
+ },
405
+ });
406
+
407
+ return handled;
408
+ }
409
+ }