@emmvish/stable-request 1.6.1 → 1.6.2

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.
@@ -2,110 +2,296 @@ import { executeNonLinearWorkflow } from './execute-non-linear-workflow.js';
2
2
  import { safelyExecuteUnknownFunction } from './safely-execute-unknown-function.js';
3
3
  import { PHASE_DECISION_ACTIONS } from '../enums/index.js';
4
4
  export async function executeBranchWorkflow(context) {
5
- const { branches, workflowId, commonGatewayOptions, requestGroups, logPhaseResults, handlePhaseCompletion, handlePhaseError = () => { }, handleBranchCompletion, maxSerializableChars, workflowHookParams, sharedBuffer, stopOnFirstPhaseError, maxWorkflowIterations } = context;
5
+ const { branches, workflowId, commonGatewayOptions, requestGroups, logPhaseResults, handlePhaseCompletion, handlePhaseError = () => { }, handleBranchCompletion, handleBranchDecision, maxSerializableChars, workflowHookParams, sharedBuffer, stopOnFirstPhaseError, maxWorkflowIterations } = context;
6
6
  const branchResults = [];
7
7
  const allPhaseResults = [];
8
8
  const executionHistory = [];
9
+ const branchExecutionHistory = [];
10
+ const branchExecutionCounts = new Map();
9
11
  let totalRequests = 0;
10
12
  let successfulRequests = 0;
11
13
  let failedRequests = 0;
12
14
  let terminatedEarly = false;
13
15
  let terminationReason;
14
- const parallelBranches = [];
15
- const serialBranches = [];
16
- branches.forEach(branch => {
17
- if (branch.executeInParallel) {
18
- parallelBranches.push(branch);
16
+ let iterationCount = 0;
17
+ const mergeBranchConfig = (branch) => {
18
+ if (!branch.commonConfig) {
19
+ return commonGatewayOptions;
19
20
  }
20
- else {
21
- serialBranches.push(branch);
21
+ return {
22
+ ...commonGatewayOptions,
23
+ ...branch.commonConfig
24
+ };
25
+ };
26
+ const executeSingleBranch = async (branch, branchIndex, executionNumber) => {
27
+ const branchStartTime = Date.now();
28
+ try {
29
+ const branchConfig = mergeBranchConfig(branch);
30
+ const result = await executeNonLinearWorkflow({
31
+ phases: branch.phases,
32
+ workflowId: `${workflowId}-branch-${branch.id}`,
33
+ commonGatewayOptions: branchConfig,
34
+ requestGroups,
35
+ logPhaseResults,
36
+ handlePhaseCompletion,
37
+ handlePhaseError,
38
+ handlePhaseDecision: workflowHookParams?.handlePhaseDecision,
39
+ maxSerializableChars,
40
+ workflowHookParams,
41
+ sharedBuffer,
42
+ stopOnFirstPhaseError,
43
+ maxWorkflowIterations
44
+ });
45
+ const branchExecutionTime = Date.now() - branchStartTime;
46
+ const branchResult = {
47
+ branchId: branch.id,
48
+ branchIndex,
49
+ success: result.failedRequests === 0,
50
+ executionTime: branchExecutionTime,
51
+ completedPhases: result.phaseResults.length,
52
+ phaseResults: result.phaseResults,
53
+ executionNumber
54
+ };
55
+ return {
56
+ branchResult,
57
+ phaseResults: result.phaseResults,
58
+ executionHistory: result.executionHistory,
59
+ totalRequests: result.totalRequests,
60
+ successfulRequests: result.successfulRequests,
61
+ failedRequests: result.failedRequests
62
+ };
22
63
  }
23
- });
24
- if (parallelBranches.length > 0) {
25
- if (logPhaseResults) {
26
- console.info(`\nstable-request: [Workflow: ${workflowId}] Executing ${parallelBranches.length} branches in parallel: [${parallelBranches.map(b => b.id).join(', ')}]`);
64
+ catch (error) {
65
+ console.error(`stable-request: [Workflow: ${workflowId}] Branch ${branch.id} failed:`, error);
66
+ return {
67
+ branchResult: {
68
+ branchId: branch.id,
69
+ branchIndex,
70
+ success: false,
71
+ executionTime: Date.now() - branchStartTime,
72
+ completedPhases: 0,
73
+ phaseResults: [],
74
+ executionNumber,
75
+ error: error?.message || 'Branch execution failed',
76
+ decision: undefined
77
+ },
78
+ phaseResults: [],
79
+ executionHistory: [],
80
+ totalRequests: 0,
81
+ successfulRequests: 0,
82
+ failedRequests: 1
83
+ };
27
84
  }
28
- const parallelPromises = parallelBranches.map(async (branch) => {
29
- const branchStartTime = Date.now();
30
- try {
31
- const result = await executeNonLinearWorkflow({
32
- phases: branch.phases,
33
- workflowId: `${workflowId}-branch-${branch.id}`,
34
- commonGatewayOptions,
35
- requestGroups,
36
- logPhaseResults,
37
- handlePhaseCompletion,
38
- handlePhaseError,
39
- handlePhaseDecision: workflowHookParams?.handlePhaseDecision,
40
- maxSerializableChars,
41
- workflowHookParams,
42
- sharedBuffer,
43
- stopOnFirstPhaseError,
44
- maxWorkflowIterations
45
- });
46
- const branchExecutionTime = Date.now() - branchStartTime;
47
- const branchResult = {
85
+ };
86
+ let currentBranchId = branches[0]?.id || null;
87
+ while (currentBranchId !== null && iterationCount < maxWorkflowIterations) {
88
+ iterationCount++;
89
+ const branchIndex = branches.findIndex(b => b.id === currentBranchId);
90
+ if (branchIndex === -1) {
91
+ console.error(`stable-request: [Workflow: ${workflowId}] Branch '${currentBranchId}' not found`);
92
+ terminatedEarly = true;
93
+ terminationReason = `Branch '${currentBranchId}' not found`;
94
+ break;
95
+ }
96
+ const currentBranch = branches[branchIndex];
97
+ const executionNumber = (branchExecutionCounts.get(currentBranchId) || 0) + 1;
98
+ branchExecutionCounts.set(currentBranchId, executionNumber);
99
+ const maxReplayCount = currentBranch.maxReplayCount ?? Infinity;
100
+ if (executionNumber > maxReplayCount + 1) {
101
+ if (logPhaseResults) {
102
+ console.warn(`stable-request: [Workflow: ${workflowId}] Branch '${currentBranchId}' exceeded max replay count (${maxReplayCount}). Skipping.`);
103
+ }
104
+ const skippedResult = {
105
+ branchId: currentBranchId,
106
+ branchIndex,
107
+ success: false,
108
+ executionTime: 0,
109
+ completedPhases: 0,
110
+ phaseResults: [],
111
+ executionNumber,
112
+ skipped: true,
113
+ error: `Exceeded max replay count of ${maxReplayCount}`
114
+ };
115
+ branchResults.push(skippedResult);
116
+ branchExecutionHistory.push({
117
+ branchId: currentBranchId,
118
+ branchIndex,
119
+ executionNumber,
120
+ timestamp: new Date().toISOString(),
121
+ success: false,
122
+ executionTime: 0
123
+ });
124
+ currentBranchId = branches[branchIndex + 1]?.id || null;
125
+ continue;
126
+ }
127
+ const isConcurrent = currentBranch.markConcurrentBranch;
128
+ if (isConcurrent) {
129
+ const concurrentGroup = [
130
+ { branch: currentBranch, index: branchIndex, executionNumber }
131
+ ];
132
+ let j = branchIndex + 1;
133
+ while (j < branches.length && branches[j].markConcurrentBranch) {
134
+ const concurrentBranch = branches[j];
135
+ const concurrentExecNum = (branchExecutionCounts.get(concurrentBranch.id) || 0) + 1;
136
+ branchExecutionCounts.set(concurrentBranch.id, concurrentExecNum);
137
+ concurrentGroup.push({ branch: concurrentBranch, index: j, executionNumber: concurrentExecNum });
138
+ j++;
139
+ }
140
+ if (logPhaseResults) {
141
+ const branchIds = concurrentGroup.map(({ branch }) => branch.id).join(', ');
142
+ console.info(`\nstable-request: [Workflow: ${workflowId}] Executing ${concurrentGroup.length} branches in parallel: [${branchIds}]`);
143
+ }
144
+ const groupPromises = concurrentGroup.map(({ branch, index, executionNumber }) => executeSingleBranch(branch, index, executionNumber));
145
+ const groupResults = await Promise.all(groupPromises);
146
+ const concurrentBranchResults = [];
147
+ let concurrentGroupJumpTarget = null;
148
+ let concurrentGroupSkipTarget = null;
149
+ let shouldTerminate = false;
150
+ for (let k = 0; k < groupResults.length; k++) {
151
+ const result = groupResults[k];
152
+ const { branch, index, executionNumber } = concurrentGroup[k];
153
+ concurrentBranchResults.push(result.branchResult);
154
+ branchResults.push(result.branchResult);
155
+ allPhaseResults.push(...result.phaseResults);
156
+ executionHistory.push(...result.executionHistory);
157
+ totalRequests += result.totalRequests;
158
+ successfulRequests += result.successfulRequests;
159
+ failedRequests += result.failedRequests;
160
+ branchExecutionHistory.push({
48
161
  branchId: branch.id,
49
- success: result.failedRequests === 0,
50
- executionTime: branchExecutionTime,
51
- completedPhases: result.phaseResults.length,
52
- phaseResults: result.phaseResults
53
- };
162
+ branchIndex: index,
163
+ executionNumber,
164
+ timestamp: new Date().toISOString(),
165
+ success: result.branchResult.success,
166
+ executionTime: result.branchResult.executionTime
167
+ });
168
+ if (handleBranchCompletion) {
169
+ try {
170
+ await safelyExecuteUnknownFunction(handleBranchCompletion, {
171
+ branchId: result.branchResult.branchId,
172
+ branchResults: result.branchResult.phaseResults,
173
+ success: result.branchResult.success
174
+ });
175
+ }
176
+ catch (hookError) {
177
+ console.error(`stable-request: [Workflow: ${workflowId}] Error in handleBranchCompletion hook:`, hookError);
178
+ }
179
+ }
54
180
  if (branch.branchDecisionHook) {
55
181
  try {
56
182
  const decision = await safelyExecuteUnknownFunction(branch.branchDecisionHook, {
57
183
  workflowId,
58
184
  branchResults: result.phaseResults,
59
185
  branchId: branch.id,
186
+ branchIndex: index,
187
+ executionNumber,
60
188
  executionHistory: result.executionHistory,
189
+ branchExecutionHistory,
61
190
  sharedBuffer,
62
- params: workflowHookParams?.handleBranchDecisionParams
191
+ params: workflowHookParams?.handleBranchDecisionParams,
192
+ concurrentBranchResults: concurrentBranchResults
63
193
  });
64
- branchResult.decision = decision;
65
- if (decision.action === PHASE_DECISION_ACTIONS.TERMINATE) {
66
- terminatedEarly = true;
67
- terminationReason = decision.metadata?.reason || `Branch ${branch.id} terminated workflow`;
194
+ result.branchResult.decision = decision;
195
+ const historyRecord = branchExecutionHistory.find(h => h.branchId === branch.id && h.executionNumber === executionNumber);
196
+ if (historyRecord) {
197
+ historyRecord.decision = decision;
198
+ }
199
+ if (handleBranchDecision) {
200
+ try {
201
+ await safelyExecuteUnknownFunction(handleBranchDecision, decision, result.branchResult);
202
+ }
203
+ catch (hookError) {
204
+ console.error(`stable-request: [Workflow: ${workflowId}] Error in handleBranchDecision hook:`, hookError);
205
+ }
206
+ }
207
+ if (k === groupResults.length - 1) {
208
+ if (decision.action === PHASE_DECISION_ACTIONS.TERMINATE) {
209
+ shouldTerminate = true;
210
+ terminationReason = decision.metadata?.reason || `Branch ${branch.id} terminated workflow`;
211
+ }
212
+ else if (decision.action === PHASE_DECISION_ACTIONS.JUMP) {
213
+ concurrentGroupJumpTarget = decision.targetBranchId || null;
214
+ }
215
+ else if (decision.action === PHASE_DECISION_ACTIONS.SKIP) {
216
+ concurrentGroupSkipTarget = decision.targetBranchId || null;
217
+ }
218
+ if (logPhaseResults && decision.action !== PHASE_DECISION_ACTIONS.CONTINUE) {
219
+ console.info(`stable-request: [Workflow: ${workflowId}] Concurrent group decision: ${decision.action}`, decision.targetBranchId ? `-> ${decision.targetBranchId}` : '');
220
+ }
68
221
  }
69
222
  }
70
223
  catch (decisionError) {
71
224
  console.error(`stable-request: [Workflow: ${workflowId}] Error in branch decision hook for ${branch.id}:`, decisionError);
72
225
  }
73
226
  }
74
- return {
75
- branchResult,
76
- phaseResults: result.phaseResults,
77
- executionHistory: result.executionHistory,
78
- totalRequests: result.totalRequests,
79
- successfulRequests: result.successfulRequests,
80
- failedRequests: result.failedRequests
81
- };
227
+ if (stopOnFirstPhaseError && result.failedRequests > 0) {
228
+ shouldTerminate = true;
229
+ terminationReason = `Branch ${branch.id} in concurrent group failed`;
230
+ break;
231
+ }
82
232
  }
83
- catch (error) {
84
- console.error(`stable-request: [Workflow: ${workflowId}] Branch ${branch.id} failed:`, error);
85
- return {
86
- branchResult: {
87
- branchId: branch.id,
88
- success: false,
89
- executionTime: Date.now() - branchStartTime,
90
- completedPhases: 0,
91
- phaseResults: []
92
- },
93
- phaseResults: [],
94
- executionHistory: [],
95
- totalRequests: 0,
96
- successfulRequests: 0,
97
- failedRequests: 1
98
- };
233
+ if (shouldTerminate) {
234
+ terminatedEarly = true;
235
+ break;
99
236
  }
100
- });
101
- const parallelResults = await Promise.all(parallelPromises);
102
- for (const result of parallelResults) {
237
+ if (concurrentGroupJumpTarget) {
238
+ currentBranchId = concurrentGroupJumpTarget;
239
+ }
240
+ else if (concurrentGroupSkipTarget) {
241
+ const skipTargetIndex = branches.findIndex(b => b.id === concurrentGroupSkipTarget);
242
+ if (skipTargetIndex !== -1) {
243
+ for (let skipIdx = j; skipIdx < skipTargetIndex; skipIdx++) {
244
+ const skippedBranch = branches[skipIdx];
245
+ const skippedResult = {
246
+ branchId: skippedBranch.id,
247
+ branchIndex: skipIdx,
248
+ success: true,
249
+ executionTime: 0,
250
+ completedPhases: 0,
251
+ phaseResults: [],
252
+ executionNumber: 1,
253
+ skipped: true
254
+ };
255
+ branchResults.push(skippedResult);
256
+ branchExecutionHistory.push({
257
+ branchId: skippedBranch.id,
258
+ branchIndex: skipIdx,
259
+ executionNumber: 1,
260
+ timestamp: new Date().toISOString(),
261
+ success: true,
262
+ executionTime: 0,
263
+ decision: { action: PHASE_DECISION_ACTIONS.SKIP }
264
+ });
265
+ }
266
+ currentBranchId = concurrentGroupSkipTarget;
267
+ }
268
+ else {
269
+ currentBranchId = branches[j]?.id || null;
270
+ }
271
+ }
272
+ else {
273
+ currentBranchId = branches[j]?.id || null;
274
+ }
275
+ }
276
+ else {
277
+ if (logPhaseResults) {
278
+ console.info(`\nstable-request: [Workflow: ${workflowId}] Executing branch: ${currentBranch.id} (execution #${executionNumber})`);
279
+ }
280
+ const result = await executeSingleBranch(currentBranch, branchIndex, executionNumber);
103
281
  branchResults.push(result.branchResult);
104
282
  allPhaseResults.push(...result.phaseResults);
105
283
  executionHistory.push(...result.executionHistory);
106
284
  totalRequests += result.totalRequests;
107
285
  successfulRequests += result.successfulRequests;
108
286
  failedRequests += result.failedRequests;
287
+ branchExecutionHistory.push({
288
+ branchId: currentBranchId,
289
+ branchIndex,
290
+ executionNumber,
291
+ timestamp: new Date().toISOString(),
292
+ success: result.branchResult.success,
293
+ executionTime: result.branchResult.executionTime
294
+ });
109
295
  if (handleBranchCompletion) {
110
296
  try {
111
297
  await safelyExecuteUnknownFunction(handleBranchCompletion, {
@@ -118,154 +304,162 @@ export async function executeBranchWorkflow(context) {
118
304
  console.error(`stable-request: [Workflow: ${workflowId}] Error in handleBranchCompletion hook:`, hookError);
119
305
  }
120
306
  }
121
- }
122
- if (terminatedEarly) {
123
- return {
124
- branchResults,
125
- allPhaseResults,
126
- executionHistory,
127
- totalRequests,
128
- successfulRequests,
129
- failedRequests,
130
- terminatedEarly,
131
- terminationReason
132
- };
133
- }
134
- }
135
- let branchIndex = 0;
136
- while (branchIndex < serialBranches.length) {
137
- const branch = serialBranches[branchIndex];
138
- if (logPhaseResults) {
139
- console.info(`\nstable-request: [Workflow: ${workflowId}] Executing branch: ${branch.id}`);
140
- }
141
- const branchStartTime = Date.now();
142
- try {
143
- const result = await executeNonLinearWorkflow({
144
- phases: branch.phases,
145
- workflowId: `${workflowId}-branch-${branch.id}`,
146
- commonGatewayOptions,
147
- requestGroups,
148
- logPhaseResults,
149
- handlePhaseCompletion,
150
- handlePhaseError,
151
- handlePhaseDecision: workflowHookParams?.handlePhaseDecision,
152
- maxSerializableChars,
153
- workflowHookParams,
154
- sharedBuffer,
155
- stopOnFirstPhaseError,
156
- maxWorkflowIterations
157
- });
158
- const branchExecutionTime = Date.now() - branchStartTime;
159
- const branchResult = {
160
- branchId: branch.id,
161
- success: result.failedRequests === 0,
162
- executionTime: branchExecutionTime,
163
- completedPhases: result.phaseResults.length,
164
- phaseResults: result.phaseResults
165
- };
166
- let shouldJump = false;
167
- let jumpTargetIndex = -1;
168
- if (branch.branchDecisionHook) {
307
+ let decision = { action: PHASE_DECISION_ACTIONS.CONTINUE };
308
+ if (currentBranch.branchDecisionHook) {
169
309
  try {
170
- const decision = await safelyExecuteUnknownFunction(branch.branchDecisionHook, {
310
+ decision = await safelyExecuteUnknownFunction(currentBranch.branchDecisionHook, {
171
311
  workflowId,
172
312
  branchResults: result.phaseResults,
173
- branchId: branch.id,
313
+ branchId: currentBranch.id,
314
+ branchIndex,
315
+ executionNumber,
174
316
  executionHistory: result.executionHistory,
317
+ branchExecutionHistory,
175
318
  sharedBuffer,
176
- params: workflowHookParams?.handleBranchDecisionParams,
177
- parallelBranchResults: branchResults
178
- .filter(br => parallelBranches.some(pb => pb.id === br.branchId))
179
- .map(br => br.phaseResults)
319
+ params: workflowHookParams?.handleBranchDecisionParams
180
320
  });
181
- branchResult.decision = decision;
182
- if (decision.action === PHASE_DECISION_ACTIONS.TERMINATE) {
183
- terminatedEarly = true;
184
- terminationReason = decision.metadata?.reason || `Branch ${branch.id} terminated workflow`;
185
- branchResults.push(branchResult);
186
- allPhaseResults.push(...result.phaseResults);
187
- executionHistory.push(...result.executionHistory);
188
- totalRequests += result.totalRequests;
189
- successfulRequests += result.successfulRequests;
190
- failedRequests += result.failedRequests;
191
- break;
321
+ result.branchResult.decision = decision;
322
+ const historyRecord = branchExecutionHistory.find(h => h.branchId === currentBranchId && h.executionNumber === executionNumber);
323
+ if (historyRecord) {
324
+ historyRecord.decision = decision;
192
325
  }
193
- if (decision.action === PHASE_DECISION_ACTIONS.JUMP && decision.targetBranchId) {
194
- const targetBranchIndex = serialBranches.findIndex(b => b.id === decision.targetBranchId);
195
- if (targetBranchIndex !== -1 && targetBranchIndex > branchIndex) {
196
- if (logPhaseResults) {
197
- console.info(`stable-request: [Workflow: ${workflowId}] Jumping from branch ${branch.id} to ${decision.targetBranchId}`);
198
- }
199
- shouldJump = true;
200
- jumpTargetIndex = targetBranchIndex;
201
- }
202
- else if (targetBranchIndex === -1) {
203
- console.warn(`stable-request: [Workflow: ${workflowId}] Target branch ${decision.targetBranchId} not found`);
326
+ if (logPhaseResults) {
327
+ console.info(`stable-request: [Workflow: ${workflowId}] Branch '${currentBranchId}' decision: ${decision.action}`, decision.targetBranchId ? `-> ${decision.targetBranchId}` : '');
328
+ }
329
+ if (handleBranchDecision) {
330
+ try {
331
+ await safelyExecuteUnknownFunction(handleBranchDecision, decision, result.branchResult);
204
332
  }
205
- else if (targetBranchIndex <= branchIndex) {
206
- console.warn(`stable-request: [Workflow: ${workflowId}] Cannot jump backwards to ${decision.targetBranchId}`);
333
+ catch (hookError) {
334
+ console.error(`stable-request: [Workflow: ${workflowId}] Error in handleBranchDecision hook:`, hookError);
207
335
  }
208
336
  }
209
337
  }
210
338
  catch (decisionError) {
211
- console.error(`stable-request: [Workflow: ${workflowId}] Error in branch decision hook for ${branch.id}:`, decisionError);
212
- }
213
- }
214
- branchResults.push(branchResult);
215
- allPhaseResults.push(...result.phaseResults);
216
- executionHistory.push(...result.executionHistory);
217
- totalRequests += result.totalRequests;
218
- successfulRequests += result.successfulRequests;
219
- failedRequests += result.failedRequests;
220
- if (handleBranchCompletion) {
221
- try {
222
- await safelyExecuteUnknownFunction(handleBranchCompletion, {
223
- branchId: branchResult.branchId,
224
- branchResults: branchResult.phaseResults,
225
- success: branchResult.success
226
- });
227
- }
228
- catch (hookError) {
229
- console.error(`stable-request: [Workflow: ${workflowId}] Error in handleBranchCompletion hook:`, hookError);
339
+ console.error(`stable-request: [Workflow: ${workflowId}] Error in branch decision hook for ${currentBranch.id}:`, decisionError);
340
+ decision = { action: PHASE_DECISION_ACTIONS.CONTINUE };
230
341
  }
231
342
  }
232
- if (shouldJump) {
233
- // Jump to target branch (skip intermediate branches)
234
- branchIndex = jumpTargetIndex;
235
- continue;
343
+ switch (decision.action) {
344
+ case PHASE_DECISION_ACTIONS.TERMINATE:
345
+ terminatedEarly = true;
346
+ terminationReason = decision.metadata?.reason || `Branch ${currentBranchId} terminated workflow`;
347
+ currentBranchId = null;
348
+ break;
349
+ case PHASE_DECISION_ACTIONS.JUMP:
350
+ if (decision.targetBranchId) {
351
+ const targetIndex = branches.findIndex(b => b.id === decision.targetBranchId);
352
+ if (targetIndex === -1) {
353
+ console.error(`stable-request: [Workflow: ${workflowId}] Jump target branch '${decision.targetBranchId}' not found`);
354
+ terminatedEarly = true;
355
+ terminationReason = `Jump target branch '${decision.targetBranchId}' not found`;
356
+ currentBranchId = null;
357
+ }
358
+ else {
359
+ currentBranchId = decision.targetBranchId;
360
+ }
361
+ }
362
+ else {
363
+ currentBranchId = branches[branchIndex + 1]?.id || null;
364
+ }
365
+ break;
366
+ case PHASE_DECISION_ACTIONS.SKIP:
367
+ if (!currentBranch.allowSkip && currentBranch.allowSkip !== undefined) {
368
+ console.warn(`stable-request: [Workflow: ${workflowId}] Branch '${currentBranchId}' attempted to skip but allowSkip is false. Continuing normally.`);
369
+ currentBranchId = branches[branchIndex + 1]?.id || null;
370
+ break;
371
+ }
372
+ if (decision.targetBranchId) {
373
+ const skipTargetIndex = branches.findIndex(b => b.id === decision.targetBranchId);
374
+ if (skipTargetIndex !== -1) {
375
+ for (let skipIdx = branchIndex + 1; skipIdx < skipTargetIndex; skipIdx++) {
376
+ const skippedBranch = branches[skipIdx];
377
+ const skippedResult = {
378
+ branchId: skippedBranch.id,
379
+ branchIndex: skipIdx,
380
+ success: true,
381
+ executionTime: 0,
382
+ completedPhases: 0,
383
+ phaseResults: [],
384
+ executionNumber: 1,
385
+ skipped: true
386
+ };
387
+ branchResults.push(skippedResult);
388
+ branchExecutionHistory.push({
389
+ branchId: skippedBranch.id,
390
+ branchIndex: skipIdx,
391
+ executionNumber: 1,
392
+ timestamp: new Date().toISOString(),
393
+ success: true,
394
+ executionTime: 0,
395
+ decision: { action: PHASE_DECISION_ACTIONS.SKIP }
396
+ });
397
+ }
398
+ currentBranchId = decision.targetBranchId;
399
+ }
400
+ else {
401
+ currentBranchId = branches[branchIndex + 1]?.id || null;
402
+ }
403
+ }
404
+ else {
405
+ const nextBranch = branches[branchIndex + 1];
406
+ if (nextBranch) {
407
+ const skippedResult = {
408
+ branchId: nextBranch.id,
409
+ branchIndex: branchIndex + 1,
410
+ success: true,
411
+ executionTime: 0,
412
+ completedPhases: 0,
413
+ phaseResults: [],
414
+ executionNumber: 1,
415
+ skipped: true
416
+ };
417
+ branchResults.push(skippedResult);
418
+ branchExecutionHistory.push({
419
+ branchId: nextBranch.id,
420
+ branchIndex: branchIndex + 1,
421
+ executionNumber: 1,
422
+ timestamp: new Date().toISOString(),
423
+ success: true,
424
+ executionTime: 0,
425
+ decision: { action: PHASE_DECISION_ACTIONS.SKIP }
426
+ });
427
+ }
428
+ currentBranchId = branches[branchIndex + 2]?.id || null;
429
+ }
430
+ break;
431
+ case PHASE_DECISION_ACTIONS.REPLAY:
432
+ if (!currentBranch.allowReplay && currentBranch.allowReplay !== undefined) {
433
+ console.warn(`stable-request: [Workflow: ${workflowId}] Branch '${currentBranchId}' attempted to replay but allowReplay is false. Continuing normally.`);
434
+ currentBranchId = branches[branchIndex + 1]?.id || null;
435
+ break;
436
+ }
437
+ currentBranchId = currentBranch.id;
438
+ break;
439
+ case PHASE_DECISION_ACTIONS.CONTINUE:
440
+ default:
441
+ currentBranchId = branches[branchIndex + 1]?.id || null;
442
+ break;
236
443
  }
237
444
  if (stopOnFirstPhaseError && result.failedRequests > 0) {
238
- if (logPhaseResults) {
239
- console.error(`stable-request: [Workflow: ${workflowId}] Branch ${branch.id} has failures. Stopping workflow.`);
240
- }
241
445
  terminatedEarly = true;
242
- terminationReason = `Branch ${branch.id} failed`;
446
+ terminationReason = `Branch ${currentBranch.id} failed`;
243
447
  break;
244
448
  }
245
449
  }
246
- catch (error) {
247
- console.error(`stable-request: [Workflow: ${workflowId}] Branch ${branch.id} failed:`, error);
248
- const branchResult = {
249
- branchId: branch.id,
250
- success: false,
251
- executionTime: Date.now() - branchStartTime,
252
- completedPhases: 0,
253
- phaseResults: []
254
- };
255
- branchResults.push(branchResult);
256
- failedRequests += 1;
257
- if (stopOnFirstPhaseError) {
258
- terminatedEarly = true;
259
- terminationReason = `Branch ${branch.id} failed with error`;
260
- break;
261
- }
450
+ }
451
+ if (iterationCount >= maxWorkflowIterations) {
452
+ terminatedEarly = true;
453
+ terminationReason = `Exceeded maximum workflow iterations (${maxWorkflowIterations})`;
454
+ if (logPhaseResults) {
455
+ console.warn(`stable-request: [Workflow: ${workflowId}] ${terminationReason}`);
262
456
  }
263
- branchIndex++;
264
457
  }
265
458
  return {
266
459
  branchResults,
267
460
  allPhaseResults,
268
461
  executionHistory,
462
+ branchExecutionHistory,
269
463
  totalRequests,
270
464
  successfulRequests,
271
465
  failedRequests,