mcp-event-intelligence 0.1.1 → 0.2.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 +57 -0
- package/README.md +67 -4
- package/bin/mcp-event-intelligence.mjs +1 -1
- package/dist/src/composite/derivedGraph.d.ts +4 -0
- package/dist/src/composite/derivedGraph.js +46 -0
- package/dist/src/composite/engine.d.ts +3 -64
- package/dist/src/composite/engine.js +26 -343
- package/dist/src/composite/matching.d.ts +8 -0
- package/dist/src/composite/matching.js +109 -0
- package/dist/src/composite/runtimeSupport.d.ts +24 -0
- package/dist/src/composite/runtimeSupport.js +176 -0
- package/dist/src/composite/store.d.ts +59 -0
- package/dist/src/composite/store.js +1 -0
- package/dist/src/intelligenceProtocol/schemas.d.ts +9 -7
- package/dist/src/intelligenceProtocol/schemas.js +1 -0
- package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +6 -6
- package/dist/src/mcpEvents/erpnextProvider.d.ts +20 -0
- package/dist/src/mcpEvents/erpnextProvider.js +80 -0
- package/package.json +5 -1
- package/scripts/host-integration.d.mts +34 -4
- package/scripts/host-integration.mjs +44 -1
- package/scripts/lib/composite-wake-coordinator.mjs +233 -9
- package/scripts/lib/event-processor.mjs +235 -20
- package/scripts/lib/local-event-intelligence-runtime.mjs +227 -82
- package/scripts/lib/mcp-events-client.mjs +173 -66
- package/scripts/lib/persistent-event-store.mjs +308 -1
- package/scripts/lib/wake-retry-scheduler.mjs +94 -0
- package/scripts/mcp-stdio-server.mjs +1 -1
- package/scripts/service.mjs +20 -0
- package/server.json +2 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import {
|
|
2
3
|
WakeRecordSchema,
|
|
3
4
|
sha256Hex,
|
|
@@ -11,6 +12,13 @@ async function stableId(prefix, value) {
|
|
|
11
12
|
return `${prefix}_${hash.slice(0, 24)}`;
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
function retryDelayMs(attemptCount, baseDelayMs, maxDelayMs) {
|
|
16
|
+
return Math.min(
|
|
17
|
+
maxDelayMs,
|
|
18
|
+
baseDelayMs * (2 ** Math.max(0, attemptCount - 1)),
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
export class CompositeWakeCoordinator {
|
|
15
23
|
constructor({
|
|
16
24
|
store,
|
|
@@ -18,12 +26,91 @@ export class CompositeWakeCoordinator {
|
|
|
18
26
|
deliverer = null,
|
|
19
27
|
packetBuilder = buildGenericRuntimeWakePacket,
|
|
20
28
|
now = () => new Date(),
|
|
29
|
+
workerId = `wake-worker:${randomUUID()}`,
|
|
30
|
+
leaseMs = 30000,
|
|
31
|
+
maxAttempts = 5,
|
|
32
|
+
retryBaseDelayMs = 1000,
|
|
33
|
+
retryMaxDelayMs = 60000,
|
|
21
34
|
}) {
|
|
22
35
|
this.store = store;
|
|
23
36
|
this.triggerEngine = triggerEngine;
|
|
24
37
|
this.deliverer = deliverer;
|
|
25
38
|
this.packetBuilder = packetBuilder;
|
|
26
39
|
this.now = now;
|
|
40
|
+
this.workerId = workerId;
|
|
41
|
+
this.leaseMs = Math.max(1000, Number(leaseMs) || 30000);
|
|
42
|
+
this.maxAttempts = Math.max(1, Number(maxAttempts) || 5);
|
|
43
|
+
this.retryBaseDelayMs = Math.max(1, Number(retryBaseDelayMs) || 1000);
|
|
44
|
+
this.retryMaxDelayMs = Math.max(
|
|
45
|
+
this.retryBaseDelayMs,
|
|
46
|
+
Number(retryMaxDelayMs) || 60000,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
for (const method of [
|
|
50
|
+
'ensureWakeDelivery',
|
|
51
|
+
'getWakeDelivery',
|
|
52
|
+
'claimWakeDelivery',
|
|
53
|
+
'completeWakeDelivery',
|
|
54
|
+
'failWakeDelivery',
|
|
55
|
+
]) {
|
|
56
|
+
if (typeof store?.[method] !== 'function') {
|
|
57
|
+
const error = new Error(
|
|
58
|
+
`Wake coordinator store requires ${method}() for durable delivery`,
|
|
59
|
+
);
|
|
60
|
+
error.code = 'WAKE_DELIVERY_STORE_UNSUPPORTED';
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async deliveredResult({ match, wakeId, definition, delivery }) {
|
|
67
|
+
const existing = this.store.latestWake(wakeId);
|
|
68
|
+
let delivered = existing;
|
|
69
|
+
if (
|
|
70
|
+
!delivered ||
|
|
71
|
+
(delivered.status !== 'delivered' && delivered.status !== 'handled')
|
|
72
|
+
) {
|
|
73
|
+
delivered = WakeRecordSchema.parse({
|
|
74
|
+
wakeId,
|
|
75
|
+
traceId: match.sourceEvents[0]?.traceId ?? match.matchId,
|
|
76
|
+
decisionId: null,
|
|
77
|
+
subscriptionId: `trigger:${match.triggerId}`,
|
|
78
|
+
sourceEventId: match.matchId,
|
|
79
|
+
createdAt: this.now().toISOString(),
|
|
80
|
+
target: definition.target,
|
|
81
|
+
status: 'delivered',
|
|
82
|
+
runtimeReceiptId: delivery.runtimeReceiptId,
|
|
83
|
+
});
|
|
84
|
+
await this.store.appendWake(delivered);
|
|
85
|
+
await this.store.appendAudit({
|
|
86
|
+
auditId: await stableId(
|
|
87
|
+
'audit',
|
|
88
|
+
`${wakeId}:delivered-recovered:${this.store.auditLength()}`,
|
|
89
|
+
),
|
|
90
|
+
traceId: delivered.traceId,
|
|
91
|
+
timestamp: this.now().toISOString(),
|
|
92
|
+
kind: 'wake.delivered',
|
|
93
|
+
entityType: 'wake',
|
|
94
|
+
entityId: wakeId,
|
|
95
|
+
fromState: 'wake_queued',
|
|
96
|
+
toState: 'wake_delivered',
|
|
97
|
+
details: {
|
|
98
|
+
triggerMatchId: match.matchId,
|
|
99
|
+
runtimeReceiptId: delivered.runtimeReceiptId ?? null,
|
|
100
|
+
recoveredFromDeliveryState: true,
|
|
101
|
+
composite: true,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (match.status !== 'fired') {
|
|
107
|
+
await this.triggerEngine.markFired(match.matchId, wakeId);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
status: delivered.status === 'handled' ? 'handled' : 'wake_delivered',
|
|
112
|
+
wake: delivered,
|
|
113
|
+
};
|
|
27
114
|
}
|
|
28
115
|
|
|
29
116
|
async deliverMatched(match) {
|
|
@@ -61,16 +148,19 @@ export class CompositeWakeCoordinator {
|
|
|
61
148
|
`composite:${match.matchId}:${definition.target.runtime}:${definition.target.id}`,
|
|
62
149
|
);
|
|
63
150
|
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
151
|
+
const existingWake = this.store.latestWake(wakeId);
|
|
152
|
+
if (existingWake?.status === 'handled' || existingWake?.status === 'delivered') {
|
|
66
153
|
if (match.status !== 'fired') {
|
|
67
154
|
await this.triggerEngine.markFired(match.matchId, wakeId);
|
|
68
155
|
}
|
|
69
156
|
return {
|
|
70
|
-
status:
|
|
71
|
-
wake:
|
|
157
|
+
status: existingWake.status === 'handled' ? 'handled' : 'wake_delivered',
|
|
158
|
+
wake: existingWake,
|
|
72
159
|
};
|
|
73
160
|
}
|
|
161
|
+
if (existingWake?.status === 'dead_letter') {
|
|
162
|
+
return { status: 'dead_letter', wake: existingWake };
|
|
163
|
+
}
|
|
74
164
|
|
|
75
165
|
const queued = WakeRecordSchema.parse({
|
|
76
166
|
wakeId,
|
|
@@ -83,8 +173,88 @@ export class CompositeWakeCoordinator {
|
|
|
83
173
|
status: 'queued',
|
|
84
174
|
});
|
|
85
175
|
|
|
86
|
-
if (!
|
|
176
|
+
if (!this.deliverer) {
|
|
177
|
+
if (!existingWake) {
|
|
178
|
+
await this.store.appendWake(queued);
|
|
179
|
+
await this.store.appendAudit({
|
|
180
|
+
auditId: await stableId(
|
|
181
|
+
'audit',
|
|
182
|
+
`${wakeId}:queued:${this.store.auditLength()}`,
|
|
183
|
+
),
|
|
184
|
+
traceId: queued.traceId,
|
|
185
|
+
timestamp: this.now().toISOString(),
|
|
186
|
+
kind: 'wake.queued',
|
|
187
|
+
entityType: 'wake',
|
|
188
|
+
entityId: wakeId,
|
|
189
|
+
toState: 'wake_queued',
|
|
190
|
+
details: {
|
|
191
|
+
triggerMatchId: match.matchId,
|
|
192
|
+
triggerId: match.triggerId,
|
|
193
|
+
composite: true,
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
status: 'wake_queued',
|
|
199
|
+
wake: this.store.latestWake(wakeId) ?? queued,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const nowIso = this.now().toISOString();
|
|
204
|
+
await this.store.ensureWakeDelivery({
|
|
205
|
+
wakeId,
|
|
206
|
+
matchId: match.matchId,
|
|
207
|
+
triggerId: match.triggerId,
|
|
208
|
+
triggerVersion: match.triggerVersion,
|
|
209
|
+
runtime: definition.target.runtime,
|
|
210
|
+
now: nowIso,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const currentDelivery = this.store.getWakeDelivery(wakeId);
|
|
214
|
+
if (currentDelivery?.status === 'delivered') {
|
|
215
|
+
return this.deliveredResult({
|
|
216
|
+
match,
|
|
217
|
+
wakeId,
|
|
218
|
+
definition,
|
|
219
|
+
delivery: currentDelivery,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (currentDelivery?.status === 'dead_letter') {
|
|
223
|
+
const deadLetter = existingWake?.status === 'dead_letter'
|
|
224
|
+
? existingWake
|
|
225
|
+
: WakeRecordSchema.parse({
|
|
226
|
+
...queued,
|
|
227
|
+
createdAt: this.now().toISOString(),
|
|
228
|
+
status: 'dead_letter',
|
|
229
|
+
});
|
|
230
|
+
if (existingWake?.status !== 'dead_letter') {
|
|
231
|
+
await this.store.appendWake(deadLetter);
|
|
232
|
+
}
|
|
233
|
+
return { status: 'dead_letter', wake: deadLetter };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const claim = await this.store.claimWakeDelivery(wakeId, {
|
|
237
|
+
workerId: this.workerId,
|
|
238
|
+
now: nowIso,
|
|
239
|
+
leaseMs: this.leaseMs,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (!claim) {
|
|
243
|
+
const delivery = this.store.getWakeDelivery(wakeId);
|
|
244
|
+
return {
|
|
245
|
+
status:
|
|
246
|
+
delivery?.status === 'claimed'
|
|
247
|
+
? 'delivery_in_progress'
|
|
248
|
+
: 'retry_scheduled',
|
|
249
|
+
wake: this.store.latestWake(wakeId) ?? queued,
|
|
250
|
+
delivery,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let persistedQueued = this.store.latestWake(wakeId);
|
|
255
|
+
if (!persistedQueued) {
|
|
87
256
|
await this.store.appendWake(queued);
|
|
257
|
+
persistedQueued = queued;
|
|
88
258
|
await this.store.appendAudit({
|
|
89
259
|
auditId: await stableId(
|
|
90
260
|
'audit',
|
|
@@ -104,10 +274,6 @@ export class CompositeWakeCoordinator {
|
|
|
104
274
|
});
|
|
105
275
|
}
|
|
106
276
|
|
|
107
|
-
if (!this.deliverer) {
|
|
108
|
-
return { status: 'wake_queued', wake: queued };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
277
|
try {
|
|
112
278
|
const packet = this.packetBuilder({
|
|
113
279
|
wakeId,
|
|
@@ -115,6 +281,19 @@ export class CompositeWakeCoordinator {
|
|
|
115
281
|
definition,
|
|
116
282
|
});
|
|
117
283
|
const receipt = await this.deliverer(packet);
|
|
284
|
+
if (
|
|
285
|
+
!receipt ||
|
|
286
|
+
typeof receipt.runtimeReceiptId !== 'string' ||
|
|
287
|
+
!receipt.runtimeReceiptId
|
|
288
|
+
) {
|
|
289
|
+
throw new Error('Wake deliverer must return runtimeReceiptId');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const delivery = await this.store.completeWakeDelivery(wakeId, {
|
|
293
|
+
workerId: this.workerId,
|
|
294
|
+
runtimeReceiptId: receipt.runtimeReceiptId,
|
|
295
|
+
now: this.now().toISOString(),
|
|
296
|
+
});
|
|
118
297
|
|
|
119
298
|
const delivered = WakeRecordSchema.parse({
|
|
120
299
|
...queued,
|
|
@@ -139,6 +318,7 @@ export class CompositeWakeCoordinator {
|
|
|
139
318
|
triggerMatchId: match.matchId,
|
|
140
319
|
runtimeReceiptId: delivered.runtimeReceiptId,
|
|
141
320
|
duplicate: receipt.duplicate === true,
|
|
321
|
+
attemptCount: delivery?.attemptCount ?? claim.attemptCount,
|
|
142
322
|
composite: true,
|
|
143
323
|
},
|
|
144
324
|
});
|
|
@@ -149,8 +329,50 @@ export class CompositeWakeCoordinator {
|
|
|
149
329
|
status: 'wake_delivered',
|
|
150
330
|
wake: delivered,
|
|
151
331
|
packet,
|
|
332
|
+
delivery,
|
|
152
333
|
};
|
|
153
334
|
} catch (error) {
|
|
335
|
+
const delay = retryDelayMs(
|
|
336
|
+
claim.attemptCount,
|
|
337
|
+
this.retryBaseDelayMs,
|
|
338
|
+
this.retryMaxDelayMs,
|
|
339
|
+
);
|
|
340
|
+
const failureAt = this.now();
|
|
341
|
+
const delivery = await this.store.failWakeDelivery(wakeId, {
|
|
342
|
+
workerId: this.workerId,
|
|
343
|
+
error,
|
|
344
|
+
now: failureAt.toISOString(),
|
|
345
|
+
maxAttempts: this.maxAttempts,
|
|
346
|
+
nextAttemptAt: new Date(failureAt.getTime() + delay).toISOString(),
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
if (delivery?.status !== 'dead_letter') {
|
|
350
|
+
await this.store.appendAudit({
|
|
351
|
+
auditId: await stableId(
|
|
352
|
+
'audit',
|
|
353
|
+
`${wakeId}:retry:${delivery?.attemptCount ?? claim.attemptCount}:${this.store.auditLength()}`,
|
|
354
|
+
),
|
|
355
|
+
traceId: queued.traceId,
|
|
356
|
+
timestamp: this.now().toISOString(),
|
|
357
|
+
kind: 'wake.retry_scheduled',
|
|
358
|
+
entityType: 'wake',
|
|
359
|
+
entityId: wakeId,
|
|
360
|
+
details: {
|
|
361
|
+
triggerMatchId: match.matchId,
|
|
362
|
+
attemptCount: delivery?.attemptCount ?? claim.attemptCount,
|
|
363
|
+
nextAttemptAt: delivery?.nextAttemptAt ?? null,
|
|
364
|
+
message: error instanceof Error ? error.message : 'wake delivery failed',
|
|
365
|
+
composite: true,
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
return {
|
|
369
|
+
status: 'retry_scheduled',
|
|
370
|
+
wake: persistedQueued,
|
|
371
|
+
delivery,
|
|
372
|
+
error,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
154
376
|
const deadLetter = WakeRecordSchema.parse({
|
|
155
377
|
...queued,
|
|
156
378
|
createdAt: this.now().toISOString(),
|
|
@@ -171,6 +393,7 @@ export class CompositeWakeCoordinator {
|
|
|
171
393
|
toState: 'dead_letter',
|
|
172
394
|
details: {
|
|
173
395
|
triggerMatchId: match.matchId,
|
|
396
|
+
attemptCount: delivery?.attemptCount ?? claim.attemptCount,
|
|
174
397
|
message: error instanceof Error ? error.message : 'wake delivery failed',
|
|
175
398
|
composite: true,
|
|
176
399
|
},
|
|
@@ -178,6 +401,7 @@ export class CompositeWakeCoordinator {
|
|
|
178
401
|
return {
|
|
179
402
|
status: 'dead_letter',
|
|
180
403
|
wake: deadLetter,
|
|
404
|
+
delivery,
|
|
181
405
|
error,
|
|
182
406
|
};
|
|
183
407
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import {
|
|
2
3
|
assertDecisionLineage,
|
|
3
4
|
assertWakeLineage,
|
|
@@ -20,17 +21,37 @@ async function stableId(prefix, value) {
|
|
|
20
21
|
return `${prefix}_${hash.slice(0, 24)}`;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
function retryDelayMs(attemptCount, baseDelayMs, maxDelayMs) {
|
|
25
|
+
return Math.min(
|
|
26
|
+
maxDelayMs,
|
|
27
|
+
baseDelayMs * (2 ** Math.max(0, attemptCount - 1)),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
export class EventProcessor {
|
|
24
32
|
constructor({
|
|
25
33
|
store,
|
|
26
34
|
evaluator = null,
|
|
27
35
|
wakeDeliverer = null,
|
|
28
36
|
now = () => new Date(),
|
|
37
|
+
workerId = `event-wake-worker:${randomUUID()}`,
|
|
38
|
+
leaseMs = 30000,
|
|
39
|
+
maxAttempts = 5,
|
|
40
|
+
retryBaseDelayMs = 1000,
|
|
41
|
+
retryMaxDelayMs = 60000,
|
|
29
42
|
}) {
|
|
30
43
|
this.store = store;
|
|
31
44
|
this.evaluator = evaluator;
|
|
32
45
|
this.wakeDeliverer = wakeDeliverer;
|
|
33
46
|
this.now = now;
|
|
47
|
+
this.workerId = workerId;
|
|
48
|
+
this.leaseMs = Math.max(1000, Number(leaseMs) || 30000);
|
|
49
|
+
this.maxAttempts = Math.max(1, Number(maxAttempts) || 5);
|
|
50
|
+
this.retryBaseDelayMs = Math.max(1, Number(retryBaseDelayMs) || 1000);
|
|
51
|
+
this.retryMaxDelayMs = Math.max(
|
|
52
|
+
this.retryBaseDelayMs,
|
|
53
|
+
Number(retryMaxDelayMs) || 60000,
|
|
54
|
+
);
|
|
34
55
|
}
|
|
35
56
|
|
|
36
57
|
async ingest(rawRequest) {
|
|
@@ -307,73 +328,267 @@ export class EventProcessor {
|
|
|
307
328
|
};
|
|
308
329
|
}
|
|
309
330
|
|
|
331
|
+
const deliveryResult = await this.deliverQueuedWake({
|
|
332
|
+
wake: queuedWake,
|
|
333
|
+
lineage,
|
|
334
|
+
event: request.event,
|
|
335
|
+
decision,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
traceId,
|
|
340
|
+
sourceEventId: request.event.eventId,
|
|
341
|
+
status: deliveryResult.status,
|
|
342
|
+
decision,
|
|
343
|
+
wake: deliveryResult.wake,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async deliveredWakeFromState(wake, delivery) {
|
|
348
|
+
const latest = this.store.latestWake(wake.wakeId);
|
|
349
|
+
if (latest?.status === 'handled' || latest?.status === 'delivered') {
|
|
350
|
+
return latest;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const delivered = WakeRecordSchema.parse({
|
|
354
|
+
...wake,
|
|
355
|
+
createdAt: nowIso(this.now),
|
|
356
|
+
status: 'delivered',
|
|
357
|
+
runtimeReceiptId: delivery.runtimeReceiptId ?? undefined,
|
|
358
|
+
});
|
|
359
|
+
await this.store.appendWake(delivered);
|
|
360
|
+
await this.store.appendAudit({
|
|
361
|
+
auditId: await stableId(
|
|
362
|
+
'audit',
|
|
363
|
+
`${wake.traceId}:wake-delivered-recovered:${this.store.auditLength()}`,
|
|
364
|
+
),
|
|
365
|
+
traceId: wake.traceId,
|
|
366
|
+
timestamp: nowIso(this.now),
|
|
367
|
+
kind: 'wake.delivered',
|
|
368
|
+
entityType: 'wake',
|
|
369
|
+
entityId: wake.wakeId,
|
|
370
|
+
fromState: 'wake_queued',
|
|
371
|
+
toState: 'wake_delivered',
|
|
372
|
+
details: {
|
|
373
|
+
runtimeReceiptId: delivered.runtimeReceiptId ?? null,
|
|
374
|
+
recoveredFromDeliveryState: true,
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
return delivered;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async deliverQueuedWake({ wake, lineage, event, decision }) {
|
|
381
|
+
if (!this.wakeDeliverer) {
|
|
382
|
+
return { status: 'wake_queued', wake };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const now = nowIso(this.now);
|
|
386
|
+
await this.store.ensureWakeDelivery({
|
|
387
|
+
wakeId: wake.wakeId,
|
|
388
|
+
sourceType: 'event',
|
|
389
|
+
sourceId: wake.traceId,
|
|
390
|
+
runtime: wake.target.runtime,
|
|
391
|
+
now,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const current = this.store.getWakeDelivery(wake.wakeId);
|
|
395
|
+
if (current?.status === 'delivered') {
|
|
396
|
+
return {
|
|
397
|
+
status: 'wake_delivered',
|
|
398
|
+
wake: await this.deliveredWakeFromState(wake, current),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
if (current?.status === 'dead_letter') {
|
|
402
|
+
return {
|
|
403
|
+
status: 'dead_letter',
|
|
404
|
+
wake: this.store.latestWake(wake.wakeId) ?? wake,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const claim = await this.store.claimWakeDelivery(wake.wakeId, {
|
|
409
|
+
workerId: this.workerId,
|
|
410
|
+
now,
|
|
411
|
+
leaseMs: this.leaseMs,
|
|
412
|
+
});
|
|
413
|
+
if (!claim) {
|
|
414
|
+
const delivery = this.store.getWakeDelivery(wake.wakeId);
|
|
415
|
+
return {
|
|
416
|
+
status:
|
|
417
|
+
delivery?.status === 'claimed'
|
|
418
|
+
? 'delivery_in_progress'
|
|
419
|
+
: 'retry_scheduled',
|
|
420
|
+
wake: this.store.latestWake(wake.wakeId) ?? wake,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
310
424
|
try {
|
|
311
425
|
const receipt = await this.wakeDeliverer({
|
|
312
|
-
wake
|
|
426
|
+
wake,
|
|
313
427
|
lineage,
|
|
314
|
-
event
|
|
428
|
+
event,
|
|
315
429
|
decision,
|
|
316
430
|
});
|
|
431
|
+
if (
|
|
432
|
+
!receipt ||
|
|
433
|
+
typeof receipt.runtimeReceiptId !== 'string' ||
|
|
434
|
+
!receipt.runtimeReceiptId
|
|
435
|
+
) {
|
|
436
|
+
throw new Error('Wake deliverer must return runtimeReceiptId');
|
|
437
|
+
}
|
|
317
438
|
|
|
439
|
+
const delivery = await this.store.completeWakeDelivery(wake.wakeId, {
|
|
440
|
+
workerId: this.workerId,
|
|
441
|
+
runtimeReceiptId: receipt.runtimeReceiptId,
|
|
442
|
+
now: nowIso(this.now),
|
|
443
|
+
});
|
|
318
444
|
const deliveredWake = WakeRecordSchema.parse({
|
|
319
|
-
...
|
|
445
|
+
...wake,
|
|
320
446
|
createdAt: nowIso(this.now),
|
|
321
447
|
status: 'delivered',
|
|
322
|
-
runtimeReceiptId: receipt
|
|
448
|
+
runtimeReceiptId: receipt.runtimeReceiptId,
|
|
323
449
|
});
|
|
324
450
|
await this.store.appendWake(deliveredWake);
|
|
325
451
|
await this.store.appendAudit({
|
|
326
|
-
auditId: await stableId(
|
|
327
|
-
|
|
452
|
+
auditId: await stableId(
|
|
453
|
+
'audit',
|
|
454
|
+
`${wake.traceId}:wake-delivered:${this.store.auditLength()}`,
|
|
455
|
+
),
|
|
456
|
+
traceId: wake.traceId,
|
|
328
457
|
timestamp: nowIso(this.now),
|
|
329
458
|
kind: 'wake.delivered',
|
|
330
459
|
entityType: 'wake',
|
|
331
|
-
entityId: wakeId,
|
|
460
|
+
entityId: wake.wakeId,
|
|
332
461
|
fromState: 'wake_queued',
|
|
333
462
|
toState: 'wake_delivered',
|
|
334
463
|
details: {
|
|
335
464
|
runtimeReceiptId: deliveredWake.runtimeReceiptId ?? null,
|
|
465
|
+
attemptCount: delivery?.attemptCount ?? claim.attemptCount,
|
|
336
466
|
},
|
|
337
467
|
});
|
|
338
|
-
|
|
339
468
|
return {
|
|
340
|
-
traceId,
|
|
341
|
-
sourceEventId: request.event.eventId,
|
|
342
469
|
status: 'wake_delivered',
|
|
343
|
-
decision,
|
|
344
470
|
wake: deliveredWake,
|
|
345
471
|
};
|
|
346
472
|
} catch (error) {
|
|
473
|
+
const failedAt = this.now();
|
|
474
|
+
const delay = retryDelayMs(
|
|
475
|
+
claim.attemptCount,
|
|
476
|
+
this.retryBaseDelayMs,
|
|
477
|
+
this.retryMaxDelayMs,
|
|
478
|
+
);
|
|
479
|
+
const delivery = await this.store.failWakeDelivery(wake.wakeId, {
|
|
480
|
+
workerId: this.workerId,
|
|
481
|
+
error,
|
|
482
|
+
now: failedAt.toISOString(),
|
|
483
|
+
maxAttempts: this.maxAttempts,
|
|
484
|
+
nextAttemptAt: new Date(failedAt.getTime() + delay).toISOString(),
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
if (delivery?.status !== 'dead_letter') {
|
|
488
|
+
await this.store.appendAudit({
|
|
489
|
+
auditId: await stableId(
|
|
490
|
+
'audit',
|
|
491
|
+
`${wake.traceId}:wake-retry:${delivery?.attemptCount ?? claim.attemptCount}:${this.store.auditLength()}`,
|
|
492
|
+
),
|
|
493
|
+
traceId: wake.traceId,
|
|
494
|
+
timestamp: nowIso(this.now),
|
|
495
|
+
kind: 'wake.retry_scheduled',
|
|
496
|
+
entityType: 'wake',
|
|
497
|
+
entityId: wake.wakeId,
|
|
498
|
+
details: {
|
|
499
|
+
attemptCount: delivery?.attemptCount ?? claim.attemptCount,
|
|
500
|
+
nextAttemptAt: delivery?.nextAttemptAt ?? null,
|
|
501
|
+
message:
|
|
502
|
+
error instanceof Error
|
|
503
|
+
? error.message
|
|
504
|
+
: 'wake delivery failed',
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
return {
|
|
508
|
+
status: 'retry_scheduled',
|
|
509
|
+
wake: this.store.latestWake(wake.wakeId) ?? wake,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
347
513
|
const deadLetter = WakeRecordSchema.parse({
|
|
348
|
-
...
|
|
514
|
+
...wake,
|
|
349
515
|
createdAt: nowIso(this.now),
|
|
350
516
|
status: 'dead_letter',
|
|
351
517
|
});
|
|
352
518
|
await this.store.appendWake(deadLetter);
|
|
353
519
|
await this.store.appendAudit({
|
|
354
|
-
auditId: await stableId(
|
|
355
|
-
|
|
520
|
+
auditId: await stableId(
|
|
521
|
+
'audit',
|
|
522
|
+
`${wake.traceId}:wake-dead-letter:${this.store.auditLength()}`,
|
|
523
|
+
),
|
|
524
|
+
traceId: wake.traceId,
|
|
356
525
|
timestamp: nowIso(this.now),
|
|
357
526
|
kind: 'wake.dead_letter',
|
|
358
527
|
entityType: 'wake',
|
|
359
|
-
entityId: wakeId,
|
|
528
|
+
entityId: wake.wakeId,
|
|
360
529
|
fromState: 'wake_queued',
|
|
361
530
|
toState: 'dead_letter',
|
|
362
531
|
details: {
|
|
363
|
-
|
|
532
|
+
attemptCount: delivery?.attemptCount ?? claim.attemptCount,
|
|
533
|
+
message:
|
|
534
|
+
error instanceof Error
|
|
535
|
+
? error.message
|
|
536
|
+
: 'wake delivery failed',
|
|
364
537
|
},
|
|
365
538
|
});
|
|
366
|
-
|
|
367
539
|
return {
|
|
368
|
-
traceId,
|
|
369
|
-
sourceEventId: request.event.eventId,
|
|
370
540
|
status: 'dead_letter',
|
|
371
|
-
decision,
|
|
372
541
|
wake: deadLetter,
|
|
373
542
|
};
|
|
374
543
|
}
|
|
375
544
|
}
|
|
376
545
|
|
|
546
|
+
async retryWake(wakeId) {
|
|
547
|
+
const delivery = this.store.getWakeDelivery(wakeId);
|
|
548
|
+
if (!delivery || delivery.sourceType !== 'event') {
|
|
549
|
+
return { status: 'not_event_wake', wake: null };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const latest = this.store.latestWake(wakeId);
|
|
553
|
+
if (!latest) {
|
|
554
|
+
throw new Error(`Unknown wake: ${wakeId}`);
|
|
555
|
+
}
|
|
556
|
+
if (latest.status === 'handled') {
|
|
557
|
+
return { status: 'handled', wake: latest };
|
|
558
|
+
}
|
|
559
|
+
if (latest.status === 'dead_letter') {
|
|
560
|
+
return { status: 'dead_letter', wake: latest };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const trace = this.store.trace(delivery.sourceId);
|
|
564
|
+
const eventRecord = trace.events[0];
|
|
565
|
+
if (!eventRecord?.lineage || !eventRecord?.event) {
|
|
566
|
+
const error = new Error(
|
|
567
|
+
`Cannot reconstruct event wake ${wakeId}; trace ${delivery.sourceId} is missing`,
|
|
568
|
+
);
|
|
569
|
+
error.code = 'WAKE_RETRY_SOURCE_MISSING';
|
|
570
|
+
throw error;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const decision = latest.decisionId
|
|
574
|
+
? trace.decisions.find(
|
|
575
|
+
(candidate) => candidate.decisionId === latest.decisionId,
|
|
576
|
+
) ?? null
|
|
577
|
+
: null;
|
|
578
|
+
|
|
579
|
+
const queued = WakeRecordSchema.parse({
|
|
580
|
+
...latest,
|
|
581
|
+
status: 'queued',
|
|
582
|
+
runtimeReceiptId: undefined,
|
|
583
|
+
});
|
|
584
|
+
return this.deliverQueuedWake({
|
|
585
|
+
wake: queued,
|
|
586
|
+
lineage: eventRecord.lineage,
|
|
587
|
+
event: eventRecord.event,
|
|
588
|
+
decision,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
377
592
|
async acknowledgeWake(wakeId, runtimeReceiptId) {
|
|
378
593
|
const latest = this.store.latestWake(wakeId);
|
|
379
594
|
if (!latest) throw new Error('Unknown wake');
|