@onlineapps/conn-orch-orchestrator 1.0.47 → 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 +161 -143
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,39 +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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
//
|
|
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
|
+
});
|
|
259
257
|
|
|
260
|
-
// Add attempt to
|
|
258
|
+
// Add attempt record to step's _retry_history
|
|
261
259
|
const attemptRecord = {
|
|
262
|
-
step_id: current_step,
|
|
263
|
-
step_index: failedStepIndex,
|
|
264
260
|
attempt: attemptNumber,
|
|
265
|
-
service: serviceName,
|
|
266
261
|
error: error.message,
|
|
262
|
+
error_ref: errorRef, // Reference to full stack in logs
|
|
267
263
|
at: new Date().toISOString()
|
|
268
264
|
};
|
|
269
265
|
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
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)
|
|
274
279
|
};
|
|
275
280
|
|
|
276
281
|
this.logger.warn(`[WorkflowOrchestrator] Step failed, attempt ${attemptNumber}/${this.retryConfig.maxAttempts}`, {
|
|
@@ -278,12 +283,35 @@ class WorkflowOrchestrator {
|
|
|
278
283
|
current_step,
|
|
279
284
|
service: serviceName,
|
|
280
285
|
error: error.message,
|
|
286
|
+
error_ref: errorRef,
|
|
281
287
|
attempt: attemptNumber
|
|
282
288
|
});
|
|
283
289
|
|
|
290
|
+
// Publish progress event for EACH retry attempt
|
|
291
|
+
const { publishToMonitoringWorkflow } = require('@onlineapps/mq-client-core').monitoring;
|
|
292
|
+
try {
|
|
293
|
+
await publishToMonitoringWorkflow(this.mqClient, {
|
|
294
|
+
event_type: 'progress',
|
|
295
|
+
workflow_id: workflow_id,
|
|
296
|
+
service_name: serviceName,
|
|
297
|
+
step_index: currentIndex,
|
|
298
|
+
step_id: current_step,
|
|
299
|
+
cookbook: updatedCookbook, // UNIFIED cookbook with _retry_history in step
|
|
300
|
+
error: { message: error.message, error_ref: errorRef },
|
|
301
|
+
status: 'retry',
|
|
302
|
+
attempt: attemptNumber,
|
|
303
|
+
max_attempts: this.retryConfig.maxAttempts,
|
|
304
|
+
timestamp: new Date().toISOString()
|
|
305
|
+
}, this.logger, { workflow_id, step_id: current_step });
|
|
306
|
+
} catch (monitoringError) {
|
|
307
|
+
this.logger.warn('Failed to publish retry progress to monitoring', {
|
|
308
|
+
workflow_id,
|
|
309
|
+
error: monitoringError.message
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
284
313
|
// Check if we should retry
|
|
285
314
|
if (attemptNumber < this.retryConfig.maxAttempts) {
|
|
286
|
-
// Calculate delay with exponential backoff
|
|
287
315
|
const delay = Math.min(
|
|
288
316
|
this.retryConfig.baseDelay * Math.pow(this.retryConfig.backoffMultiplier, attemptNumber - 1),
|
|
289
317
|
this.retryConfig.maxDelay
|
|
@@ -295,16 +323,14 @@ class WorkflowOrchestrator {
|
|
|
295
323
|
delay
|
|
296
324
|
});
|
|
297
325
|
|
|
298
|
-
// Wait for delay
|
|
299
326
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
300
327
|
|
|
301
|
-
// Republish
|
|
328
|
+
// Republish with UNIFIED cookbook
|
|
302
329
|
const serviceQueue = `${serviceName}.workflow`;
|
|
303
330
|
await this.mqClient.publish(serviceQueue, {
|
|
304
331
|
workflow_id,
|
|
305
|
-
cookbook:
|
|
306
|
-
current_step
|
|
307
|
-
context: updatedContext
|
|
332
|
+
cookbook: updatedCookbook,
|
|
333
|
+
current_step
|
|
308
334
|
});
|
|
309
335
|
|
|
310
336
|
this.logger.info(`[WorkflowOrchestrator] Message republished for retry`, {
|
|
@@ -314,25 +340,27 @@ class WorkflowOrchestrator {
|
|
|
314
340
|
nextAttempt: attemptNumber + 1
|
|
315
341
|
});
|
|
316
342
|
|
|
317
|
-
// Don't throw - message will be retried
|
|
318
343
|
return { retrying: true, attempt: attemptNumber, nextAttempt: attemptNumber + 1 };
|
|
319
344
|
}
|
|
320
345
|
|
|
321
|
-
// Max retries exhausted - send to
|
|
346
|
+
// Max retries exhausted - mark step as failed and send to DLQ
|
|
322
347
|
const dlqEntryRecord = {
|
|
323
348
|
dlq_entered: true,
|
|
324
|
-
step_id: current_step,
|
|
325
|
-
step_index: failedStepIndex,
|
|
326
|
-
service: serviceName,
|
|
327
349
|
reason: 'max_retries_exceeded',
|
|
328
350
|
total_attempts: attemptNumber,
|
|
329
351
|
at: new Date().toISOString()
|
|
330
352
|
};
|
|
331
353
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
|
336
364
|
};
|
|
337
365
|
|
|
338
366
|
try {
|
|
@@ -341,41 +369,32 @@ class WorkflowOrchestrator {
|
|
|
341
369
|
workflow_id,
|
|
342
370
|
current_step,
|
|
343
371
|
attempts: attemptNumber,
|
|
344
|
-
|
|
372
|
+
error_ref: errorRef // Reference to full stack in logs
|
|
345
373
|
});
|
|
346
374
|
|
|
347
375
|
// Publish to workflow.failed (DLQ queue - message stays until manual requeue/discard)
|
|
376
|
+
// UNIFIED COOKBOOK contains everything including _retry_history in the step
|
|
348
377
|
await this.mqClient.publish('workflow.failed', {
|
|
349
378
|
workflow_id,
|
|
350
379
|
current_step,
|
|
351
|
-
|
|
352
|
-
step_index: failedStepIndex,
|
|
353
|
-
service: serviceName,
|
|
354
|
-
cookbook: cookbookDef,
|
|
355
|
-
context: finalContext,
|
|
356
|
-
error: error.message,
|
|
357
|
-
errorStack: error.stack,
|
|
358
|
-
_dlq_history: finalDlqHistory,
|
|
380
|
+
cookbook: finalCookbook, // UNIFIED cookbook with full state
|
|
359
381
|
failed_at: new Date().toISOString()
|
|
360
382
|
});
|
|
361
383
|
|
|
362
384
|
// ALSO publish to monitoring.workflow so dashboard shows the DLQ entry
|
|
363
|
-
// This is separate from the DLQ message - it's just for visibility
|
|
364
385
|
try {
|
|
365
386
|
await publishToMonitoringWorkflow(this.mqClient, {
|
|
366
387
|
event_type: 'failed',
|
|
367
388
|
workflow_id: workflow_id,
|
|
368
389
|
service_name: serviceName,
|
|
369
|
-
step_index:
|
|
390
|
+
step_index: currentIndex,
|
|
370
391
|
step_id: current_step,
|
|
371
|
-
cookbook:
|
|
372
|
-
|
|
373
|
-
error: { message: error.message },
|
|
392
|
+
cookbook: finalCookbook, // UNIFIED cookbook
|
|
393
|
+
error: { message: error.message, error_ref: errorRef },
|
|
374
394
|
status: 'failed',
|
|
375
395
|
timestamp: new Date().toISOString()
|
|
376
396
|
}, this.logger, { workflow_id, step_id: current_step });
|
|
377
397
|
} catch (monitoringError) {
|
|
378
|
-
// Don't fail if monitoring publish fails
|
|
379
398
|
this.logger.warn('[WorkflowOrchestrator] Failed to publish DLQ entry to monitoring', {
|
|
380
399
|
workflow_id,
|
|
381
400
|
error: monitoringError.message
|
|
@@ -578,17 +597,17 @@ class WorkflowOrchestrator {
|
|
|
578
597
|
* @private
|
|
579
598
|
* @async
|
|
580
599
|
* @param {Object} nextStep - Next step to execute
|
|
581
|
-
* @param {Object}
|
|
582
|
-
* @param {Object} context - Updated context
|
|
600
|
+
* @param {Object} cookbook - UNIFIED cookbook (contains everything)
|
|
583
601
|
* @param {string} workflow_id - Workflow ID
|
|
584
602
|
*/
|
|
585
|
-
async _routeToNextStep(nextStep,
|
|
603
|
+
async _routeToNextStep(nextStep, cookbook, workflow_id) {
|
|
586
604
|
const nextStepId = this._getStepId(nextStep);
|
|
605
|
+
|
|
606
|
+
// UNIFIED COOKBOOK: no separate context, everything is in cookbook
|
|
587
607
|
const message = {
|
|
588
608
|
workflow_id,
|
|
589
|
-
cookbook:
|
|
590
|
-
current_step: nextStepId
|
|
591
|
-
context
|
|
609
|
+
cookbook: cookbook,
|
|
610
|
+
current_step: nextStepId
|
|
592
611
|
};
|
|
593
612
|
|
|
594
613
|
if (nextStep.type === 'task') {
|
|
@@ -620,38 +639,37 @@ class WorkflowOrchestrator {
|
|
|
620
639
|
* @private
|
|
621
640
|
* @async
|
|
622
641
|
* @param {string} workflow_id - Workflow ID
|
|
623
|
-
* @param {Object}
|
|
642
|
+
* @param {Object} cookbook - UNIFIED cookbook (contains everything)
|
|
624
643
|
* @param {string} serviceName - Service name that completed the workflow
|
|
625
644
|
* @param {string} lastStepId - ID of the last completed step
|
|
626
645
|
* @param {number} lastStepIndex - Index of the last completed step
|
|
627
646
|
*/
|
|
628
|
-
async _completeWorkflow(workflow_id,
|
|
647
|
+
async _completeWorkflow(workflow_id, cookbook, serviceName = 'unknown', lastStepId = null, lastStepIndex = 0) {
|
|
629
648
|
try {
|
|
630
649
|
console.log(`[WorkflowOrchestrator] [PUBLISH] Preparing to publish workflow.completed for ${workflow_id}`);
|
|
631
650
|
this.logger.info(`[WorkflowOrchestrator] [PUBLISH] Preparing to publish workflow.completed`, {
|
|
632
651
|
workflow_id,
|
|
633
652
|
serviceName,
|
|
634
653
|
lastStepId,
|
|
635
|
-
lastStepIndex
|
|
636
|
-
hasMqClient: !!this.mqClient,
|
|
637
|
-
mqClientConnected: this.mqClient?._connected
|
|
654
|
+
lastStepIndex
|
|
638
655
|
});
|
|
639
656
|
|
|
640
|
-
//
|
|
641
|
-
|
|
642
|
-
|
|
657
|
+
// Update pointer to dispatcher (final step)
|
|
658
|
+
const finalCookbook = {
|
|
659
|
+
...cookbook,
|
|
660
|
+
_pointer: { next: 'api_delivery_dispatcher' }
|
|
661
|
+
};
|
|
643
662
|
|
|
644
663
|
// Build message in format expected by Delivery Dispatcher
|
|
664
|
+
// UNIFIED COOKBOOK contains everything including delivery config
|
|
645
665
|
const workflowCompletedMessage = {
|
|
646
666
|
workflow_id,
|
|
647
667
|
status: 'completed',
|
|
648
|
-
service: serviceName,
|
|
649
|
-
step_id: lastStepId,
|
|
650
|
-
step_index: lastStepIndex,
|
|
651
|
-
cookbook:
|
|
652
|
-
delivery: delivery
|
|
653
|
-
context: finalContext, // Full context for output resolution
|
|
654
|
-
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' },
|
|
655
673
|
completed_at: new Date().toISOString()
|
|
656
674
|
};
|
|
657
675
|
|