@onlineapps/conn-orch-orchestrator 1.0.48 → 1.0.49
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/package.json +1 -1
- package/src/WorkflowOrchestrator.js +143 -147
package/package.json
CHANGED
|
@@ -116,24 +116,19 @@ class WorkflowOrchestrator {
|
|
|
116
116
|
* }, 'hello-service');
|
|
117
117
|
*/
|
|
118
118
|
async processWorkflowMessage(message, serviceName) {
|
|
119
|
-
const { workflow_id, cookbook: cookbookDef, current_step
|
|
119
|
+
const { workflow_id, cookbook: cookbookDef, current_step } = message;
|
|
120
|
+
const startTime = Date.now();
|
|
120
121
|
|
|
121
122
|
try {
|
|
122
123
|
// Validate cookbook structure
|
|
123
124
|
this.cookbook.validateCookbook(cookbookDef);
|
|
124
125
|
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
const enrichedContext = {
|
|
128
|
-
...context,
|
|
129
|
-
// Preserve delivery from cookbook if not already in context
|
|
130
|
-
delivery: context?.delivery || cookbookDef?.delivery || { handler: 'none' }
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
// Find current step (supports both V1 'id' and V2 'step_id')
|
|
134
|
-
// V2 FORMAT: steps is an object keyed by step_id
|
|
126
|
+
// UNIFIED COOKBOOK: Everything is in cookbook, no separate context
|
|
127
|
+
// Find current step
|
|
135
128
|
const stepsArray = this._getStepsArray(cookbookDef);
|
|
136
|
-
const
|
|
129
|
+
const currentIndex = stepsArray.findIndex(s => this._getStepId(s) === current_step);
|
|
130
|
+
const step = stepsArray[currentIndex];
|
|
131
|
+
|
|
137
132
|
if (!step) {
|
|
138
133
|
throw new Error(`Step not found: ${current_step}`);
|
|
139
134
|
}
|
|
@@ -141,93 +136,90 @@ class WorkflowOrchestrator {
|
|
|
141
136
|
// Check if this step is for this service
|
|
142
137
|
if (step.type === 'task' && step.service !== serviceName) {
|
|
143
138
|
this.logger.warn(`Step ${current_step} is for ${step.service}, not ${serviceName}`);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
} else {
|
|
147
|
-
// Fallback: publish to service's workflow queue directly
|
|
148
|
-
const serviceQueue = `${step.service}.workflow`;
|
|
149
|
-
await this.mqClient.publish(serviceQueue, message);
|
|
150
|
-
}
|
|
139
|
+
const serviceQueue = `${step.service}.workflow`;
|
|
140
|
+
await this.mqClient.publish(serviceQueue, message);
|
|
151
141
|
return { skipped: true, reason: 'wrong_service' };
|
|
152
142
|
}
|
|
153
143
|
|
|
154
|
-
//
|
|
144
|
+
// Mark step as started
|
|
145
|
+
const updatedSteps = [...stepsArray];
|
|
146
|
+
updatedSteps[currentIndex] = {
|
|
147
|
+
...step,
|
|
148
|
+
started_at: new Date().toISOString(),
|
|
149
|
+
status: 'in_progress'
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Execute the step (pass _input from cookbook as context for operations)
|
|
153
|
+
const stepContext = {
|
|
154
|
+
...cookbookDef._input,
|
|
155
|
+
_system: cookbookDef._system,
|
|
156
|
+
delivery: cookbookDef.delivery
|
|
157
|
+
};
|
|
158
|
+
|
|
155
159
|
let result;
|
|
156
160
|
if (this.cache && step.type === 'task') {
|
|
157
|
-
const cacheKey = this._getCacheKey(step,
|
|
161
|
+
const cacheKey = this._getCacheKey(step, stepContext);
|
|
158
162
|
result = await this.cache.get(cacheKey);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
this.logger.info('Cache hit', { step: this._getStepId(step), cacheKey });
|
|
162
|
-
} else {
|
|
163
|
-
// Execute the step
|
|
164
|
-
result = await this._executeStep(step, enrichedContext, cookbookDef);
|
|
163
|
+
if (!result) {
|
|
164
|
+
result = await this._executeStep(step, stepContext, cookbookDef);
|
|
165
165
|
await this.cache.set(cacheKey, result, { ttl: 300 });
|
|
166
166
|
}
|
|
167
167
|
} else {
|
|
168
|
-
|
|
169
|
-
result = await this._executeStep(step, enrichedContext, cookbookDef);
|
|
168
|
+
result = await this._executeStep(step, stepContext, cookbookDef);
|
|
170
169
|
}
|
|
171
170
|
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
output: result, // Add output from operation
|
|
171
|
+
// Calculate duration
|
|
172
|
+
const duration_ms = Date.now() - startTime;
|
|
173
|
+
|
|
174
|
+
// Determine next step
|
|
175
|
+
const nextStep = stepsArray[currentIndex + 1];
|
|
176
|
+
const nextPointer = nextStep
|
|
177
|
+
? this._getStepId(nextStep)
|
|
178
|
+
: 'api_delivery_dispatcher'; // Last step -> dispatcher
|
|
179
|
+
|
|
180
|
+
// Update step with output and completion info
|
|
181
|
+
updatedSteps[currentIndex] = {
|
|
182
|
+
...step,
|
|
183
|
+
started_at: updatedSteps[currentIndex].started_at,
|
|
184
|
+
output: result,
|
|
187
185
|
status: 'completed',
|
|
188
|
-
completed_at: new Date().toISOString()
|
|
186
|
+
completed_at: new Date().toISOString(),
|
|
187
|
+
duration_ms
|
|
189
188
|
};
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
189
|
+
|
|
190
|
+
// Build UPDATED UNIFIED COOKBOOK
|
|
191
|
+
const updatedCookbook = {
|
|
192
|
+
...cookbookDef,
|
|
193
|
+
steps: updatedSteps,
|
|
194
|
+
_pointer: { next: nextStep ? this._getStepId(nextStep) : null }
|
|
194
195
|
};
|
|
195
196
|
|
|
196
|
-
// Publish workflow.progress
|
|
197
|
+
// Publish workflow.progress with UNIFIED COOKBOOK
|
|
197
198
|
const { publishToMonitoringWorkflow } = require('@onlineapps/mq-client-core').monitoring;
|
|
198
199
|
try {
|
|
199
|
-
console.log(`[WorkflowOrchestrator] [PROGRESS] Publishing progress for ${workflow_id}, step: ${current_step}, index: ${currentIndex}`);
|
|
200
200
|
await publishToMonitoringWorkflow(this.mqClient, {
|
|
201
201
|
event_type: 'progress',
|
|
202
202
|
workflow_id: workflow_id,
|
|
203
203
|
service_name: serviceName,
|
|
204
|
-
step_index: currentIndex
|
|
204
|
+
step_index: currentIndex,
|
|
205
205
|
step_id: current_step,
|
|
206
|
-
cookbook:
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
status: 'completed', // Step completed (not in_progress)
|
|
206
|
+
cookbook: updatedCookbook, // UNIFIED - contains everything
|
|
207
|
+
output: result,
|
|
208
|
+
status: 'completed',
|
|
210
209
|
timestamp: new Date().toISOString()
|
|
211
210
|
}, this.logger, { workflow_id, step_id: current_step });
|
|
212
|
-
console.log(`[WorkflowOrchestrator] [PROGRESS] ✓ Progress published for ${workflow_id}`);
|
|
213
211
|
} catch (monitoringError) {
|
|
214
|
-
// Don't fail workflow if monitoring publish fails
|
|
215
|
-
console.error(`[WorkflowOrchestrator] [PROGRESS] ✗ Failed to publish progress:`, monitoringError);
|
|
216
212
|
this.logger.warn('Failed to publish workflow.progress to monitoring', {
|
|
217
213
|
workflow_id,
|
|
218
|
-
error: monitoringError.message
|
|
214
|
+
error: monitoringError.message
|
|
219
215
|
});
|
|
220
216
|
}
|
|
221
217
|
|
|
222
|
-
//
|
|
223
|
-
const nextStep = stepsArray[currentIndex + 1];
|
|
224
|
-
|
|
218
|
+
// Route to next step or complete workflow
|
|
225
219
|
if (nextStep) {
|
|
226
|
-
|
|
227
|
-
await this._routeToNextStep(nextStep, cookbookDef, updatedContext, workflow_id);
|
|
220
|
+
await this._routeToNextStep(nextStep, updatedCookbook, workflow_id);
|
|
228
221
|
} else {
|
|
229
|
-
|
|
230
|
-
await this._completeWorkflow(workflow_id, updatedContext, cookbookDef, serviceName, current_step, currentIndex);
|
|
222
|
+
await this._completeWorkflow(workflow_id, updatedCookbook, serviceName, current_step, currentIndex);
|
|
231
223
|
}
|
|
232
224
|
|
|
233
225
|
return {
|
|
@@ -238,35 +230,52 @@ class WorkflowOrchestrator {
|
|
|
238
230
|
};
|
|
239
231
|
|
|
240
232
|
} catch (error) {
|
|
241
|
-
//
|
|
242
|
-
const
|
|
243
|
-
const stepAttempts = dlqHistory.filter(h => h.step_id === current_step && h.attempt).length;
|
|
244
|
-
const attemptNumber = stepAttempts + 1;
|
|
233
|
+
// UNIFIED COOKBOOK: _retry_history is stored INSIDE the step, not globally
|
|
234
|
+
const crypto = require('crypto');
|
|
245
235
|
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
236
|
+
// Find current step index
|
|
237
|
+
const stepsArray = this._getStepsArray(cookbookDef);
|
|
238
|
+
const currentIndex = stepsArray.findIndex(s => this._getStepId(s) === current_step);
|
|
239
|
+
const currentStepDef = stepsArray[currentIndex] || {};
|
|
240
|
+
|
|
241
|
+
// Get existing retry history from the step itself
|
|
242
|
+
const stepRetryHistory = currentStepDef._retry_history || [];
|
|
243
|
+
const attemptNumber = stepRetryHistory.length + 1;
|
|
244
|
+
|
|
245
|
+
// Generate error reference for log lookup (instead of full stack trace)
|
|
246
|
+
const errorRef = `err-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
247
|
+
|
|
248
|
+
// Log full stack trace with error_ref for debugging
|
|
249
|
+
this.logger.error(`[${errorRef}] Step execution failed`, {
|
|
250
|
+
workflow_id,
|
|
251
|
+
step_id: current_step,
|
|
252
|
+
service: serviceName,
|
|
253
|
+
attempt: attemptNumber,
|
|
254
|
+
error: error.message,
|
|
255
|
+
stack: error.stack
|
|
256
|
+
});
|
|
255
257
|
|
|
256
|
-
// Add attempt to
|
|
258
|
+
// Add attempt record to step's _retry_history
|
|
257
259
|
const attemptRecord = {
|
|
258
|
-
step_id: current_step,
|
|
259
|
-
step_index: failedStepIndex,
|
|
260
260
|
attempt: attemptNumber,
|
|
261
|
-
service: serviceName,
|
|
262
261
|
error: error.message,
|
|
262
|
+
error_ref: errorRef, // Reference to full stack in logs
|
|
263
263
|
at: new Date().toISOString()
|
|
264
264
|
};
|
|
265
265
|
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
266
|
+
// Update steps array with retry history
|
|
267
|
+
const updatedSteps = [...stepsArray];
|
|
268
|
+
updatedSteps[currentIndex] = {
|
|
269
|
+
...currentStepDef,
|
|
270
|
+
status: 'retrying',
|
|
271
|
+
_retry_history: [...stepRetryHistory, attemptRecord]
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// Build updated UNIFIED COOKBOOK
|
|
275
|
+
const updatedCookbook = {
|
|
276
|
+
...cookbookDef,
|
|
277
|
+
steps: updatedSteps,
|
|
278
|
+
_pointer: { next: current_step } // Still pointing to this step (retrying)
|
|
270
279
|
};
|
|
271
280
|
|
|
272
281
|
this.logger.warn(`[WorkflowOrchestrator] Step failed, attempt ${attemptNumber}/${this.retryConfig.maxAttempts}`, {
|
|
@@ -274,29 +283,27 @@ class WorkflowOrchestrator {
|
|
|
274
283
|
current_step,
|
|
275
284
|
service: serviceName,
|
|
276
285
|
error: error.message,
|
|
286
|
+
error_ref: errorRef,
|
|
277
287
|
attempt: attemptNumber
|
|
278
288
|
});
|
|
279
289
|
|
|
280
|
-
// Publish progress event for EACH retry attempt
|
|
290
|
+
// Publish progress event for EACH retry attempt
|
|
281
291
|
const { publishToMonitoringWorkflow } = require('@onlineapps/mq-client-core').monitoring;
|
|
282
292
|
try {
|
|
283
293
|
await publishToMonitoringWorkflow(this.mqClient, {
|
|
284
294
|
event_type: 'progress',
|
|
285
295
|
workflow_id: workflow_id,
|
|
286
296
|
service_name: serviceName,
|
|
287
|
-
step_index:
|
|
297
|
+
step_index: currentIndex,
|
|
288
298
|
step_id: current_step,
|
|
289
|
-
cookbook:
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
error: { message: error.message },
|
|
293
|
-
status: 'retry', // Mark as retry attempt
|
|
299
|
+
cookbook: updatedCookbook, // UNIFIED cookbook with _retry_history in step
|
|
300
|
+
error: { message: error.message, error_ref: errorRef },
|
|
301
|
+
status: 'retry',
|
|
294
302
|
attempt: attemptNumber,
|
|
295
303
|
max_attempts: this.retryConfig.maxAttempts,
|
|
296
304
|
timestamp: new Date().toISOString()
|
|
297
305
|
}, this.logger, { workflow_id, step_id: current_step });
|
|
298
306
|
} catch (monitoringError) {
|
|
299
|
-
// Don't fail if monitoring publish fails
|
|
300
307
|
this.logger.warn('Failed to publish retry progress to monitoring', {
|
|
301
308
|
workflow_id,
|
|
302
309
|
error: monitoringError.message
|
|
@@ -305,7 +312,6 @@ class WorkflowOrchestrator {
|
|
|
305
312
|
|
|
306
313
|
// Check if we should retry
|
|
307
314
|
if (attemptNumber < this.retryConfig.maxAttempts) {
|
|
308
|
-
// Calculate delay with exponential backoff
|
|
309
315
|
const delay = Math.min(
|
|
310
316
|
this.retryConfig.baseDelay * Math.pow(this.retryConfig.backoffMultiplier, attemptNumber - 1),
|
|
311
317
|
this.retryConfig.maxDelay
|
|
@@ -317,16 +323,14 @@ class WorkflowOrchestrator {
|
|
|
317
323
|
delay
|
|
318
324
|
});
|
|
319
325
|
|
|
320
|
-
// Wait for delay
|
|
321
326
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
322
327
|
|
|
323
|
-
// Republish
|
|
328
|
+
// Republish with UNIFIED cookbook
|
|
324
329
|
const serviceQueue = `${serviceName}.workflow`;
|
|
325
330
|
await this.mqClient.publish(serviceQueue, {
|
|
326
331
|
workflow_id,
|
|
327
|
-
cookbook:
|
|
328
|
-
current_step
|
|
329
|
-
context: updatedContext
|
|
332
|
+
cookbook: updatedCookbook,
|
|
333
|
+
current_step
|
|
330
334
|
});
|
|
331
335
|
|
|
332
336
|
this.logger.info(`[WorkflowOrchestrator] Message republished for retry`, {
|
|
@@ -336,25 +340,27 @@ class WorkflowOrchestrator {
|
|
|
336
340
|
nextAttempt: attemptNumber + 1
|
|
337
341
|
});
|
|
338
342
|
|
|
339
|
-
// Don't throw - message will be retried
|
|
340
343
|
return { retrying: true, attempt: attemptNumber, nextAttempt: attemptNumber + 1 };
|
|
341
344
|
}
|
|
342
345
|
|
|
343
|
-
// Max retries exhausted - send to
|
|
346
|
+
// Max retries exhausted - mark step as failed and send to DLQ
|
|
344
347
|
const dlqEntryRecord = {
|
|
345
348
|
dlq_entered: true,
|
|
346
|
-
step_id: current_step,
|
|
347
|
-
step_index: failedStepIndex,
|
|
348
|
-
service: serviceName,
|
|
349
349
|
reason: 'max_retries_exceeded',
|
|
350
350
|
total_attempts: attemptNumber,
|
|
351
351
|
at: new Date().toISOString()
|
|
352
352
|
};
|
|
353
353
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
354
|
+
updatedSteps[currentIndex] = {
|
|
355
|
+
...updatedSteps[currentIndex],
|
|
356
|
+
status: 'failed',
|
|
357
|
+
_retry_history: [...updatedSteps[currentIndex]._retry_history, dlqEntryRecord]
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const finalCookbook = {
|
|
361
|
+
...cookbookDef,
|
|
362
|
+
steps: updatedSteps,
|
|
363
|
+
_pointer: { next: null } // Workflow stuck - waiting for human intervention
|
|
358
364
|
};
|
|
359
365
|
|
|
360
366
|
try {
|
|
@@ -363,41 +369,32 @@ class WorkflowOrchestrator {
|
|
|
363
369
|
workflow_id,
|
|
364
370
|
current_step,
|
|
365
371
|
attempts: attemptNumber,
|
|
366
|
-
|
|
372
|
+
error_ref: errorRef // Reference to full stack in logs
|
|
367
373
|
});
|
|
368
374
|
|
|
369
375
|
// Publish to workflow.failed (DLQ queue - message stays until manual requeue/discard)
|
|
376
|
+
// UNIFIED COOKBOOK contains everything including _retry_history in the step
|
|
370
377
|
await this.mqClient.publish('workflow.failed', {
|
|
371
378
|
workflow_id,
|
|
372
379
|
current_step,
|
|
373
|
-
|
|
374
|
-
step_index: failedStepIndex,
|
|
375
|
-
service: serviceName,
|
|
376
|
-
cookbook: cookbookDef,
|
|
377
|
-
context: finalContext,
|
|
378
|
-
error: error.message,
|
|
379
|
-
errorStack: error.stack,
|
|
380
|
-
_dlq_history: finalDlqHistory,
|
|
380
|
+
cookbook: finalCookbook, // UNIFIED cookbook with full state
|
|
381
381
|
failed_at: new Date().toISOString()
|
|
382
382
|
});
|
|
383
383
|
|
|
384
384
|
// ALSO publish to monitoring.workflow so dashboard shows the DLQ entry
|
|
385
|
-
// This is separate from the DLQ message - it's just for visibility
|
|
386
385
|
try {
|
|
387
386
|
await publishToMonitoringWorkflow(this.mqClient, {
|
|
388
387
|
event_type: 'failed',
|
|
389
388
|
workflow_id: workflow_id,
|
|
390
389
|
service_name: serviceName,
|
|
391
|
-
step_index:
|
|
390
|
+
step_index: currentIndex,
|
|
392
391
|
step_id: current_step,
|
|
393
|
-
cookbook:
|
|
394
|
-
|
|
395
|
-
error: { message: error.message },
|
|
392
|
+
cookbook: finalCookbook, // UNIFIED cookbook
|
|
393
|
+
error: { message: error.message, error_ref: errorRef },
|
|
396
394
|
status: 'failed',
|
|
397
395
|
timestamp: new Date().toISOString()
|
|
398
396
|
}, this.logger, { workflow_id, step_id: current_step });
|
|
399
397
|
} catch (monitoringError) {
|
|
400
|
-
// Don't fail if monitoring publish fails
|
|
401
398
|
this.logger.warn('[WorkflowOrchestrator] Failed to publish DLQ entry to monitoring', {
|
|
402
399
|
workflow_id,
|
|
403
400
|
error: monitoringError.message
|
|
@@ -600,17 +597,17 @@ class WorkflowOrchestrator {
|
|
|
600
597
|
* @private
|
|
601
598
|
* @async
|
|
602
599
|
* @param {Object} nextStep - Next step to execute
|
|
603
|
-
* @param {Object}
|
|
604
|
-
* @param {Object} context - Updated context
|
|
600
|
+
* @param {Object} cookbook - UNIFIED cookbook (contains everything)
|
|
605
601
|
* @param {string} workflow_id - Workflow ID
|
|
606
602
|
*/
|
|
607
|
-
async _routeToNextStep(nextStep,
|
|
603
|
+
async _routeToNextStep(nextStep, cookbook, workflow_id) {
|
|
608
604
|
const nextStepId = this._getStepId(nextStep);
|
|
605
|
+
|
|
606
|
+
// UNIFIED COOKBOOK: no separate context, everything is in cookbook
|
|
609
607
|
const message = {
|
|
610
608
|
workflow_id,
|
|
611
|
-
cookbook:
|
|
612
|
-
current_step: nextStepId
|
|
613
|
-
context
|
|
609
|
+
cookbook: cookbook,
|
|
610
|
+
current_step: nextStepId
|
|
614
611
|
};
|
|
615
612
|
|
|
616
613
|
if (nextStep.type === 'task') {
|
|
@@ -642,38 +639,37 @@ class WorkflowOrchestrator {
|
|
|
642
639
|
* @private
|
|
643
640
|
* @async
|
|
644
641
|
* @param {string} workflow_id - Workflow ID
|
|
645
|
-
* @param {Object}
|
|
642
|
+
* @param {Object} cookbook - UNIFIED cookbook (contains everything)
|
|
646
643
|
* @param {string} serviceName - Service name that completed the workflow
|
|
647
644
|
* @param {string} lastStepId - ID of the last completed step
|
|
648
645
|
* @param {number} lastStepIndex - Index of the last completed step
|
|
649
646
|
*/
|
|
650
|
-
async _completeWorkflow(workflow_id,
|
|
647
|
+
async _completeWorkflow(workflow_id, cookbook, serviceName = 'unknown', lastStepId = null, lastStepIndex = 0) {
|
|
651
648
|
try {
|
|
652
649
|
console.log(`[WorkflowOrchestrator] [PUBLISH] Preparing to publish workflow.completed for ${workflow_id}`);
|
|
653
650
|
this.logger.info(`[WorkflowOrchestrator] [PUBLISH] Preparing to publish workflow.completed`, {
|
|
654
651
|
workflow_id,
|
|
655
652
|
serviceName,
|
|
656
653
|
lastStepId,
|
|
657
|
-
lastStepIndex
|
|
658
|
-
hasMqClient: !!this.mqClient,
|
|
659
|
-
mqClientConnected: this.mqClient?._connected
|
|
654
|
+
lastStepIndex
|
|
660
655
|
});
|
|
661
656
|
|
|
662
|
-
//
|
|
663
|
-
|
|
664
|
-
|
|
657
|
+
// Update pointer to dispatcher (final step)
|
|
658
|
+
const finalCookbook = {
|
|
659
|
+
...cookbook,
|
|
660
|
+
_pointer: { next: 'api_delivery_dispatcher' }
|
|
661
|
+
};
|
|
665
662
|
|
|
666
663
|
// Build message in format expected by Delivery Dispatcher
|
|
664
|
+
// UNIFIED COOKBOOK contains everything including delivery config
|
|
667
665
|
const workflowCompletedMessage = {
|
|
668
666
|
workflow_id,
|
|
669
667
|
status: 'completed',
|
|
670
|
-
service: serviceName,
|
|
671
|
-
step_id: lastStepId,
|
|
672
|
-
step_index: lastStepIndex,
|
|
673
|
-
cookbook:
|
|
674
|
-
delivery: delivery
|
|
675
|
-
context: finalContext, // Full context for output resolution
|
|
676
|
-
steps: finalContext?.steps || [], // Steps results (array) for output resolution
|
|
668
|
+
service: serviceName,
|
|
669
|
+
step_id: lastStepId,
|
|
670
|
+
step_index: lastStepIndex,
|
|
671
|
+
cookbook: finalCookbook, // UNIFIED cookbook with full state
|
|
672
|
+
delivery: cookbook.delivery || { handler: 'none' },
|
|
677
673
|
completed_at: new Date().toISOString()
|
|
678
674
|
};
|
|
679
675
|
|