@sudocode-ai/local-server 0.1.9 → 0.1.11
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/dist/execution/executors/agent-executor-wrapper.d.ts.map +1 -1
- package/dist/execution/executors/agent-executor-wrapper.js +57 -2
- package/dist/execution/executors/agent-executor-wrapper.js.map +1 -1
- package/dist/execution/process/builders/claude.d.ts.map +1 -1
- package/dist/execution/process/builders/claude.js +32 -1
- package/dist/execution/process/builders/claude.js.map +1 -1
- package/dist/execution/worktree/config.js +1 -1
- package/dist/execution/worktree/config.js.map +1 -1
- package/dist/execution/worktree/git-cli.d.ts +48 -0
- package/dist/execution/worktree/git-cli.d.ts.map +1 -1
- package/dist/execution/worktree/git-cli.js +81 -0
- package/dist/execution/worktree/git-cli.js.map +1 -1
- package/dist/execution/worktree/types.d.ts.map +1 -1
- package/dist/execution/worktree/types.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -4
- package/dist/index.js.map +1 -1
- package/dist/public/assets/index-Nz4IjDwB.css +1 -0
- package/dist/public/assets/index-Z8yftXvD.js +824 -0
- package/dist/public/assets/index-Z8yftXvD.js.map +1 -0
- package/dist/public/assets/{react-vendor-DiL5hC7l.js → react-vendor-5f1Wq1qs.js} +5 -5
- package/dist/public/assets/{react-vendor-DiL5hC7l.js.map → react-vendor-5f1Wq1qs.js.map} +1 -1
- package/dist/public/assets/{ui-vendor-B4WMPEfa.js → ui-vendor-BDDPoYki.js} +2 -2
- package/dist/public/assets/{ui-vendor-B4WMPEfa.js.map → ui-vendor-BDDPoYki.js.map} +1 -1
- package/dist/public/index.html +4 -4
- package/dist/routes/executions.d.ts.map +1 -1
- package/dist/routes/executions.js +3 -1
- package/dist/routes/executions.js.map +1 -1
- package/dist/routes/issues.d.ts.map +1 -1
- package/dist/routes/issues.js +13 -0
- package/dist/routes/issues.js.map +1 -1
- package/dist/routes/specs.d.ts.map +1 -1
- package/dist/routes/specs.js +14 -0
- package/dist/routes/specs.js.map +1 -1
- package/dist/routes/workflows.d.ts +8 -0
- package/dist/routes/workflows.d.ts.map +1 -0
- package/dist/routes/workflows.js +1729 -0
- package/dist/routes/workflows.js.map +1 -0
- package/dist/services/execution-event-callbacks.d.ts +73 -0
- package/dist/services/execution-event-callbacks.d.ts.map +1 -0
- package/dist/services/execution-event-callbacks.js +82 -0
- package/dist/services/execution-event-callbacks.js.map +1 -0
- package/dist/services/execution-lifecycle.d.ts +38 -2
- package/dist/services/execution-lifecycle.d.ts.map +1 -1
- package/dist/services/execution-lifecycle.js +94 -23
- package/dist/services/execution-lifecycle.js.map +1 -1
- package/dist/services/execution-service.d.ts +31 -3
- package/dist/services/execution-service.d.ts.map +1 -1
- package/dist/services/execution-service.js +161 -34
- package/dist/services/execution-service.js.map +1 -1
- package/dist/services/executions.d.ts +1 -0
- package/dist/services/executions.d.ts.map +1 -1
- package/dist/services/executions.js +4 -0
- package/dist/services/executions.js.map +1 -1
- package/dist/services/project-context.d.ts +25 -0
- package/dist/services/project-context.d.ts.map +1 -1
- package/dist/services/project-context.js +53 -3
- package/dist/services/project-context.js.map +1 -1
- package/dist/services/project-manager.d.ts +7 -0
- package/dist/services/project-manager.d.ts.map +1 -1
- package/dist/services/project-manager.js +108 -13
- package/dist/services/project-manager.js.map +1 -1
- package/dist/services/websocket.d.ts +10 -2
- package/dist/services/websocket.d.ts.map +1 -1
- package/dist/services/websocket.js +18 -0
- package/dist/services/websocket.js.map +1 -1
- package/dist/services/workflow-broadcast-service.d.ts +43 -0
- package/dist/services/workflow-broadcast-service.d.ts.map +1 -0
- package/dist/services/workflow-broadcast-service.js +145 -0
- package/dist/services/workflow-broadcast-service.js.map +1 -0
- package/dist/services/worktree-sync-service.d.ts +76 -4
- package/dist/services/worktree-sync-service.d.ts.map +1 -1
- package/dist/services/worktree-sync-service.js +264 -23
- package/dist/services/worktree-sync-service.js.map +1 -1
- package/dist/workflow/base-workflow-engine.d.ts +186 -0
- package/dist/workflow/base-workflow-engine.d.ts.map +1 -0
- package/dist/workflow/base-workflow-engine.js +549 -0
- package/dist/workflow/base-workflow-engine.js.map +1 -0
- package/dist/workflow/dependency-analyzer.d.ts +78 -0
- package/dist/workflow/dependency-analyzer.d.ts.map +1 -0
- package/dist/workflow/dependency-analyzer.js +264 -0
- package/dist/workflow/dependency-analyzer.js.map +1 -0
- package/dist/workflow/engines/orchestrator-engine.d.ts +237 -0
- package/dist/workflow/engines/orchestrator-engine.d.ts.map +1 -0
- package/dist/workflow/engines/orchestrator-engine.js +749 -0
- package/dist/workflow/engines/orchestrator-engine.js.map +1 -0
- package/dist/workflow/engines/sequential-engine.d.ts +276 -0
- package/dist/workflow/engines/sequential-engine.d.ts.map +1 -0
- package/dist/workflow/engines/sequential-engine.js +1110 -0
- package/dist/workflow/engines/sequential-engine.js.map +1 -0
- package/dist/workflow/index.d.ts +15 -0
- package/dist/workflow/index.d.ts.map +1 -0
- package/dist/workflow/index.js +22 -0
- package/dist/workflow/index.js.map +1 -0
- package/dist/workflow/mcp/api-client.d.ts +103 -0
- package/dist/workflow/mcp/api-client.d.ts.map +1 -0
- package/dist/workflow/mcp/api-client.js +193 -0
- package/dist/workflow/mcp/api-client.js.map +1 -0
- package/dist/workflow/mcp/index.d.ts +16 -0
- package/dist/workflow/mcp/index.d.ts.map +1 -0
- package/dist/workflow/mcp/index.js +114 -0
- package/dist/workflow/mcp/index.js.map +1 -0
- package/dist/workflow/mcp/server.d.ts +85 -0
- package/dist/workflow/mcp/server.d.ts.map +1 -0
- package/dist/workflow/mcp/server.js +520 -0
- package/dist/workflow/mcp/server.js.map +1 -0
- package/dist/workflow/mcp/tools/escalation.d.ts +36 -0
- package/dist/workflow/mcp/tools/escalation.d.ts.map +1 -0
- package/dist/workflow/mcp/tools/escalation.js +47 -0
- package/dist/workflow/mcp/tools/escalation.js.map +1 -0
- package/dist/workflow/mcp/tools/execution.d.ts +59 -0
- package/dist/workflow/mcp/tools/execution.d.ts.map +1 -0
- package/dist/workflow/mcp/tools/execution.js +67 -0
- package/dist/workflow/mcp/tools/execution.js.map +1 -0
- package/dist/workflow/mcp/tools/inspection.d.ts +82 -0
- package/dist/workflow/mcp/tools/inspection.d.ts.map +1 -0
- package/dist/workflow/mcp/tools/inspection.js +57 -0
- package/dist/workflow/mcp/tools/inspection.js.map +1 -0
- package/dist/workflow/mcp/tools/workflow.d.ts +59 -0
- package/dist/workflow/mcp/tools/workflow.d.ts.map +1 -0
- package/dist/workflow/mcp/tools/workflow.js +40 -0
- package/dist/workflow/mcp/tools/workflow.js.map +1 -0
- package/dist/workflow/mcp/types.d.ts +345 -0
- package/dist/workflow/mcp/types.d.ts.map +1 -0
- package/dist/workflow/mcp/types.js +7 -0
- package/dist/workflow/mcp/types.js.map +1 -0
- package/dist/workflow/services/prompt-builder.d.ts +36 -0
- package/dist/workflow/services/prompt-builder.d.ts.map +1 -0
- package/dist/workflow/services/prompt-builder.js +329 -0
- package/dist/workflow/services/prompt-builder.js.map +1 -0
- package/dist/workflow/services/wakeup-service.d.ts +262 -0
- package/dist/workflow/services/wakeup-service.d.ts.map +1 -0
- package/dist/workflow/services/wakeup-service.js +809 -0
- package/dist/workflow/services/wakeup-service.js.map +1 -0
- package/dist/workflow/workflow-engine.d.ts +221 -0
- package/dist/workflow/workflow-engine.d.ts.map +1 -0
- package/dist/workflow/workflow-engine.js +94 -0
- package/dist/workflow/workflow-engine.js.map +1 -0
- package/dist/workflow/workflow-event-emitter.d.ts +278 -0
- package/dist/workflow/workflow-event-emitter.d.ts.map +1 -0
- package/dist/workflow/workflow-event-emitter.js +259 -0
- package/dist/workflow/workflow-event-emitter.js.map +1 -0
- package/package.json +8 -6
- package/dist/public/assets/index-DV9Tbujb.css +0 -1
- package/dist/public/assets/index-DcDX9-Ad.js +0 -740
- package/dist/public/assets/index-DcDX9-Ad.js.map +0 -1
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrator Workflow Engine
|
|
3
|
+
*
|
|
4
|
+
* Agent-managed workflow execution. The orchestrator is itself an execution
|
|
5
|
+
* (Claude Code agent) that controls workflow steps via MCP tools.
|
|
6
|
+
*
|
|
7
|
+
* Key differences from SequentialWorkflowEngine:
|
|
8
|
+
* - No internal execution loop - orchestrator agent handles execution
|
|
9
|
+
* - Wakeup mechanism - events trigger follow-up executions
|
|
10
|
+
* - MCP tools for step control - orchestrator uses workflow_* and execute_* tools
|
|
11
|
+
*/
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
import { getIssue } from "@sudocode-ai/cli/dist/operations/issues.js";
|
|
17
|
+
import { BaseWorkflowEngine } from "../base-workflow-engine.js";
|
|
18
|
+
import { WorkflowCycleError, WorkflowStateError, WorkflowStepNotFoundError, } from "../workflow-engine.js";
|
|
19
|
+
import { WorkflowPromptBuilder } from "../services/prompt-builder.js";
|
|
20
|
+
import { registerExecutionCallback, } from "../../services/execution-event-callbacks.js";
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Orchestrator Workflow Engine
|
|
23
|
+
// =============================================================================
|
|
24
|
+
/**
|
|
25
|
+
* Orchestrator Workflow Engine implementation.
|
|
26
|
+
*
|
|
27
|
+
* This engine spawns an orchestrator agent (Claude Code execution) that controls
|
|
28
|
+
* the workflow using MCP tools. The orchestrator makes decisions about:
|
|
29
|
+
* - Which issues to execute
|
|
30
|
+
* - How to handle failures
|
|
31
|
+
* - When to escalate to the user
|
|
32
|
+
*
|
|
33
|
+
* Events from step executions trigger "wakeups" - follow-up messages to the
|
|
34
|
+
* orchestrator that inform it of completed work.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* const engine = new OrchestratorWorkflowEngine({
|
|
39
|
+
* db,
|
|
40
|
+
* executionService,
|
|
41
|
+
* wakeupService,
|
|
42
|
+
* config: {
|
|
43
|
+
* repoPath: '/path/to/repo',
|
|
44
|
+
* dbPath: '/path/to/.sudocode/cache.db',
|
|
45
|
+
* },
|
|
46
|
+
* });
|
|
47
|
+
*
|
|
48
|
+
* // Create workflow from a goal
|
|
49
|
+
* const workflow = await engine.createWorkflow({
|
|
50
|
+
* type: "goal",
|
|
51
|
+
* goal: "Implement user authentication with OAuth"
|
|
52
|
+
* });
|
|
53
|
+
*
|
|
54
|
+
* // Start orchestrator
|
|
55
|
+
* await engine.startWorkflow(workflow.id);
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export class OrchestratorWorkflowEngine extends BaseWorkflowEngine {
|
|
59
|
+
executionService;
|
|
60
|
+
lifecycleService;
|
|
61
|
+
wakeupService;
|
|
62
|
+
promptBuilder;
|
|
63
|
+
config;
|
|
64
|
+
unregisterExecutionCallback;
|
|
65
|
+
constructor(deps) {
|
|
66
|
+
super(deps.db, deps.eventEmitter);
|
|
67
|
+
this.executionService = deps.executionService;
|
|
68
|
+
this.lifecycleService = deps.lifecycleService;
|
|
69
|
+
this.wakeupService = deps.wakeupService;
|
|
70
|
+
this.promptBuilder = new WorkflowPromptBuilder();
|
|
71
|
+
this.config = deps.config;
|
|
72
|
+
// Register callback to record workflow events when executions complete
|
|
73
|
+
this.setupExecutionCallbacks();
|
|
74
|
+
}
|
|
75
|
+
// ===========================================================================
|
|
76
|
+
// Configuration
|
|
77
|
+
// ===========================================================================
|
|
78
|
+
/**
|
|
79
|
+
* Update the server URL after dynamic port discovery.
|
|
80
|
+
* Called by ProjectContext when the actual server port is known.
|
|
81
|
+
*/
|
|
82
|
+
setServerUrl(serverUrl) {
|
|
83
|
+
this.config.serverUrl = serverUrl;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get the wakeup service for registering await conditions.
|
|
87
|
+
* Used by the API endpoint for await_events MCP tool.
|
|
88
|
+
*/
|
|
89
|
+
getWakeupService() {
|
|
90
|
+
return this.wakeupService;
|
|
91
|
+
}
|
|
92
|
+
// ===========================================================================
|
|
93
|
+
// Execution Event Callbacks
|
|
94
|
+
// ===========================================================================
|
|
95
|
+
/**
|
|
96
|
+
* Set up callbacks to record workflow events when executions complete/fail.
|
|
97
|
+
*
|
|
98
|
+
* When an execution that belongs to a workflow completes or fails, we need to:
|
|
99
|
+
* 1. Find the corresponding workflow step
|
|
100
|
+
* 2. Update the step status
|
|
101
|
+
* 3. Record a workflow event (step_completed or step_failed)
|
|
102
|
+
* 4. Trigger a wakeup so the orchestrator can react
|
|
103
|
+
*/
|
|
104
|
+
setupExecutionCallbacks() {
|
|
105
|
+
this.unregisterExecutionCallback = registerExecutionCallback(async (event, data) => {
|
|
106
|
+
// Only handle events from workflow executions
|
|
107
|
+
if (!data.workflowId) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Get the workflow
|
|
111
|
+
const workflow = await this.getWorkflow(data.workflowId);
|
|
112
|
+
if (!workflow) {
|
|
113
|
+
console.warn(`[OrchestratorEngine] Workflow not found for execution event: ${data.workflowId}`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Find the step for this execution
|
|
117
|
+
const step = workflow.steps.find((s) => s.executionId === data.executionId);
|
|
118
|
+
if (!step) {
|
|
119
|
+
// Might be the orchestrator execution itself, not a step
|
|
120
|
+
console.debug(`[OrchestratorEngine] No step found for execution ${data.executionId} in workflow ${data.workflowId}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Clear any pending timeout for this execution
|
|
124
|
+
this.wakeupService.clearExecutionTimeout(data.executionId);
|
|
125
|
+
// Determine event type and update step status
|
|
126
|
+
const eventType = event === "completed" ? "step_completed" : "step_failed";
|
|
127
|
+
const newStepStatus = event === "completed" ? "completed" : "failed";
|
|
128
|
+
// Update step status in workflow
|
|
129
|
+
const updatedSteps = workflow.steps.map((s) => s.id === step.id
|
|
130
|
+
? { ...s, status: newStepStatus }
|
|
131
|
+
: s);
|
|
132
|
+
this.updateWorkflow(data.workflowId, { steps: updatedSteps });
|
|
133
|
+
// Record workflow event for orchestrator
|
|
134
|
+
await this.wakeupService.recordEvent({
|
|
135
|
+
workflowId: data.workflowId,
|
|
136
|
+
type: eventType,
|
|
137
|
+
executionId: data.executionId,
|
|
138
|
+
stepId: step.id,
|
|
139
|
+
payload: {
|
|
140
|
+
issueId: step.issueId,
|
|
141
|
+
error: data.error,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
// Trigger wakeup so orchestrator can react
|
|
145
|
+
await this.wakeupService.triggerWakeup(data.workflowId);
|
|
146
|
+
console.log(`[OrchestratorEngine] Recorded ${eventType} for step ${step.id} in workflow ${data.workflowId}`);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Clean up the execution callback when the engine is disposed.
|
|
151
|
+
*/
|
|
152
|
+
dispose() {
|
|
153
|
+
if (this.unregisterExecutionCallback) {
|
|
154
|
+
this.unregisterExecutionCallback();
|
|
155
|
+
this.unregisterExecutionCallback = undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// ===========================================================================
|
|
159
|
+
// Workflow Creation
|
|
160
|
+
// ===========================================================================
|
|
161
|
+
/**
|
|
162
|
+
* Create a new workflow from a source definition.
|
|
163
|
+
*
|
|
164
|
+
* For orchestrator workflows, goal-based sources create empty workflows
|
|
165
|
+
* that the orchestrator populates dynamically.
|
|
166
|
+
*
|
|
167
|
+
* @param source - How to determine workflow scope (spec, issues, root_issue, or goal)
|
|
168
|
+
* @param config - Optional configuration overrides
|
|
169
|
+
* @returns The created workflow
|
|
170
|
+
* @throws WorkflowCycleError if dependency cycles are detected
|
|
171
|
+
*/
|
|
172
|
+
async createWorkflow(source, config) {
|
|
173
|
+
// 1. Resolve source to issue IDs
|
|
174
|
+
const issueIds = await this.resolveSource(source);
|
|
175
|
+
// 2. Handle goal-based workflows (no initial issues)
|
|
176
|
+
if (source.type === "goal" && issueIds.length === 0) {
|
|
177
|
+
const workflow = this.buildWorkflow({
|
|
178
|
+
source,
|
|
179
|
+
steps: [],
|
|
180
|
+
config: config || {},
|
|
181
|
+
repoPath: this.config.repoPath,
|
|
182
|
+
});
|
|
183
|
+
this.saveWorkflow(workflow);
|
|
184
|
+
return workflow;
|
|
185
|
+
}
|
|
186
|
+
// 3. Build dependency graph
|
|
187
|
+
const graph = this.analyzeDependencies(issueIds);
|
|
188
|
+
// 4. Check for cycles
|
|
189
|
+
if (graph.cycles && graph.cycles.length > 0) {
|
|
190
|
+
throw new WorkflowCycleError(graph.cycles);
|
|
191
|
+
}
|
|
192
|
+
// 5. Create steps from graph
|
|
193
|
+
const steps = this.createStepsFromGraph(graph);
|
|
194
|
+
// 6. Build workflow object
|
|
195
|
+
const workflow = this.buildWorkflow({
|
|
196
|
+
source,
|
|
197
|
+
steps,
|
|
198
|
+
config: config || {},
|
|
199
|
+
repoPath: this.config.repoPath,
|
|
200
|
+
});
|
|
201
|
+
// 7. Save to database
|
|
202
|
+
this.saveWorkflow(workflow);
|
|
203
|
+
return workflow;
|
|
204
|
+
}
|
|
205
|
+
// ===========================================================================
|
|
206
|
+
// Workflow Lifecycle
|
|
207
|
+
// ===========================================================================
|
|
208
|
+
/**
|
|
209
|
+
* Start executing a pending workflow.
|
|
210
|
+
*
|
|
211
|
+
* Spawns an orchestrator execution (Claude Code agent) with workflow MCP tools.
|
|
212
|
+
* The orchestrator will use these tools to control step execution.
|
|
213
|
+
*
|
|
214
|
+
* @param workflowId - The workflow to start
|
|
215
|
+
* @throws WorkflowNotFoundError if workflow doesn't exist
|
|
216
|
+
* @throws WorkflowStateError if workflow is not in pending state
|
|
217
|
+
*/
|
|
218
|
+
async startWorkflow(workflowId) {
|
|
219
|
+
console.log(`[OrchestratorWorkflowEngine] startWorkflow called for ${workflowId}`);
|
|
220
|
+
const workflow = await this.getWorkflowOrThrow(workflowId);
|
|
221
|
+
// Validate state
|
|
222
|
+
if (workflow.status !== "pending") {
|
|
223
|
+
throw new WorkflowStateError(workflowId, workflow.status, "start");
|
|
224
|
+
}
|
|
225
|
+
// Create workflow-level worktree if not already present
|
|
226
|
+
if (!workflow.worktreePath) {
|
|
227
|
+
console.log(`[OrchestratorWorkflowEngine] Creating workflow worktree for ${workflowId}`);
|
|
228
|
+
const { worktreePath, branchName } = await this.createWorkflowWorktreeHelper(workflow, this.config.repoPath, this.lifecycleService);
|
|
229
|
+
// Update local workflow reference with worktree info
|
|
230
|
+
workflow.worktreePath = worktreePath;
|
|
231
|
+
workflow.branchName = branchName;
|
|
232
|
+
}
|
|
233
|
+
// Update status to running
|
|
234
|
+
const updated = this.updateWorkflow(workflowId, {
|
|
235
|
+
status: "running",
|
|
236
|
+
startedAt: new Date().toISOString(),
|
|
237
|
+
});
|
|
238
|
+
// Emit workflow started event
|
|
239
|
+
this.eventEmitter.emit({
|
|
240
|
+
type: "workflow_started",
|
|
241
|
+
workflowId,
|
|
242
|
+
workflow: updated,
|
|
243
|
+
timestamp: Date.now(),
|
|
244
|
+
});
|
|
245
|
+
// Spawn orchestrator execution
|
|
246
|
+
console.log(`[OrchestratorWorkflowEngine] Spawning orchestrator for workflow ${workflowId}`);
|
|
247
|
+
await this.spawnOrchestrator(updated);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Pause a running workflow.
|
|
251
|
+
*
|
|
252
|
+
* Sets pause status and records an event. The orchestrator will be notified
|
|
253
|
+
* via wakeup when it checks workflow status.
|
|
254
|
+
*
|
|
255
|
+
* @param workflowId - The workflow to pause
|
|
256
|
+
* @throws WorkflowNotFoundError if workflow doesn't exist
|
|
257
|
+
* @throws WorkflowStateError if workflow is not running
|
|
258
|
+
*/
|
|
259
|
+
async pauseWorkflow(workflowId) {
|
|
260
|
+
const workflow = await this.getWorkflowOrThrow(workflowId);
|
|
261
|
+
if (workflow.status !== "running") {
|
|
262
|
+
throw new WorkflowStateError(workflowId, workflow.status, "pause");
|
|
263
|
+
}
|
|
264
|
+
// Cancel ALL running executions (orchestrator chain + step executions)
|
|
265
|
+
await this.cancelAllWorkflowExecutions(workflow);
|
|
266
|
+
// Cancel pending wakeup
|
|
267
|
+
this.wakeupService.cancelPendingWakeup(workflowId);
|
|
268
|
+
// Clear any await state
|
|
269
|
+
this.wakeupService.clearAwaitState(workflowId);
|
|
270
|
+
// Update status
|
|
271
|
+
this.updateWorkflow(workflowId, { status: "paused" });
|
|
272
|
+
// Record pause event for orchestrator
|
|
273
|
+
await this.wakeupService.recordEvent({
|
|
274
|
+
workflowId,
|
|
275
|
+
type: "workflow_paused",
|
|
276
|
+
payload: {},
|
|
277
|
+
});
|
|
278
|
+
// Emit event
|
|
279
|
+
this.eventEmitter.emit({
|
|
280
|
+
type: "workflow_paused",
|
|
281
|
+
workflowId,
|
|
282
|
+
timestamp: Date.now(),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Resume a paused workflow.
|
|
287
|
+
*
|
|
288
|
+
* Uses Claude Code's session resume to continue the orchestrator from where it left off.
|
|
289
|
+
* This preserves the full conversation history and context.
|
|
290
|
+
*
|
|
291
|
+
* @param workflowId - The workflow to resume
|
|
292
|
+
* @param message - Optional message to send with the resume (defaults to "Workflow resumed. Continue execution.")
|
|
293
|
+
* @throws WorkflowNotFoundError if workflow doesn't exist
|
|
294
|
+
* @throws WorkflowStateError if workflow is not paused
|
|
295
|
+
*/
|
|
296
|
+
async resumeWorkflow(workflowId, message) {
|
|
297
|
+
const workflow = await this.getWorkflowOrThrow(workflowId);
|
|
298
|
+
if (workflow.status !== "paused") {
|
|
299
|
+
throw new WorkflowStateError(workflowId, workflow.status, "resume");
|
|
300
|
+
}
|
|
301
|
+
// Get the session ID to resume
|
|
302
|
+
const sessionId = workflow.orchestratorSessionId;
|
|
303
|
+
if (!sessionId) {
|
|
304
|
+
throw new Error(`Cannot resume workflow ${workflowId}: no orchestrator session ID found`);
|
|
305
|
+
}
|
|
306
|
+
// Update status first
|
|
307
|
+
this.updateWorkflow(workflowId, { status: "running" });
|
|
308
|
+
// Build the resume prompt
|
|
309
|
+
const resumePrompt = message || "Workflow resumed. Continue execution.";
|
|
310
|
+
// Build agent config with resume flag
|
|
311
|
+
const agentConfig = this.buildOrchestratorConfig(workflow);
|
|
312
|
+
const agentType = workflow.config.orchestratorAgentType ?? "claude-code";
|
|
313
|
+
console.log(`[OrchestratorWorkflowEngine] Resuming workflow ${workflowId} with session ${sessionId}`);
|
|
314
|
+
// Create new execution that resumes the session
|
|
315
|
+
// Link to the previous orchestrator execution for chain tracking
|
|
316
|
+
const previousExecutionId = workflow.orchestratorExecutionId;
|
|
317
|
+
const execution = await this.executionService.createExecution(null, // No issue - this is the orchestrator
|
|
318
|
+
{
|
|
319
|
+
mode: "worktree",
|
|
320
|
+
baseBranch: workflow.baseBranch,
|
|
321
|
+
reuseWorktreePath: workflow.worktreePath,
|
|
322
|
+
resume: sessionId, // Resume the previous session
|
|
323
|
+
parentExecutionId: previousExecutionId, // Link to previous execution in chain
|
|
324
|
+
...agentConfig,
|
|
325
|
+
}, resumePrompt, agentType);
|
|
326
|
+
// Update workflow with new orchestrator execution ID
|
|
327
|
+
// Note: session_id should remain the same for resumed sessions
|
|
328
|
+
this.updateWorkflow(workflowId, {
|
|
329
|
+
orchestratorExecutionId: execution.id,
|
|
330
|
+
orchestratorSessionId: execution.session_id || sessionId,
|
|
331
|
+
});
|
|
332
|
+
// Record resume event for tracking
|
|
333
|
+
await this.wakeupService.recordEvent({
|
|
334
|
+
workflowId,
|
|
335
|
+
type: "workflow_resumed",
|
|
336
|
+
payload: {
|
|
337
|
+
resumedFromSession: sessionId,
|
|
338
|
+
previousExecutionId,
|
|
339
|
+
newExecutionId: execution.id,
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
// Mark any pending events as processed (they're now part of the resumed session context)
|
|
343
|
+
const pendingEvents = this.wakeupService.getUnprocessedEvents(workflowId);
|
|
344
|
+
if (pendingEvents.length > 0) {
|
|
345
|
+
this.wakeupService.markEventsProcessed(pendingEvents.map((e) => e.id));
|
|
346
|
+
}
|
|
347
|
+
// Emit event
|
|
348
|
+
this.eventEmitter.emit({
|
|
349
|
+
type: "workflow_resumed",
|
|
350
|
+
workflowId,
|
|
351
|
+
timestamp: Date.now(),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Cancel a workflow, stopping the orchestrator and any running executions.
|
|
356
|
+
*
|
|
357
|
+
* @param workflowId - The workflow to cancel
|
|
358
|
+
* @throws WorkflowNotFoundError if workflow doesn't exist
|
|
359
|
+
* @throws WorkflowStateError if workflow is already completed/failed/cancelled
|
|
360
|
+
*/
|
|
361
|
+
async cancelWorkflow(workflowId) {
|
|
362
|
+
const workflow = await this.getWorkflowOrThrow(workflowId);
|
|
363
|
+
// Check if already in terminal state
|
|
364
|
+
if (["completed", "failed", "cancelled"].includes(workflow.status)) {
|
|
365
|
+
throw new WorkflowStateError(workflowId, workflow.status, "cancel");
|
|
366
|
+
}
|
|
367
|
+
// Cancel ALL running executions (orchestrator chain + step executions)
|
|
368
|
+
await this.cancelAllWorkflowExecutions(workflow);
|
|
369
|
+
// Cancel pending wakeup
|
|
370
|
+
this.wakeupService.cancelPendingWakeup(workflowId);
|
|
371
|
+
// Clear any await state
|
|
372
|
+
this.wakeupService.clearAwaitState(workflowId);
|
|
373
|
+
// Update status
|
|
374
|
+
this.updateWorkflow(workflowId, {
|
|
375
|
+
status: "cancelled",
|
|
376
|
+
completedAt: new Date().toISOString(),
|
|
377
|
+
});
|
|
378
|
+
// Emit event
|
|
379
|
+
this.eventEmitter.emit({
|
|
380
|
+
type: "workflow_cancelled",
|
|
381
|
+
workflowId,
|
|
382
|
+
timestamp: Date.now(),
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
// ===========================================================================
|
|
386
|
+
// Step Control
|
|
387
|
+
// ===========================================================================
|
|
388
|
+
/**
|
|
389
|
+
* Retry a failed step.
|
|
390
|
+
*
|
|
391
|
+
* Records an event for the orchestrator to handle.
|
|
392
|
+
* The orchestrator decides how to actually retry.
|
|
393
|
+
*
|
|
394
|
+
* @param workflowId - The workflow containing the step
|
|
395
|
+
* @param stepId - The step to retry
|
|
396
|
+
* @throws WorkflowNotFoundError if workflow doesn't exist
|
|
397
|
+
* @throws WorkflowStepNotFoundError if step doesn't exist
|
|
398
|
+
*/
|
|
399
|
+
async retryStep(workflowId, stepId) {
|
|
400
|
+
const workflow = await this.getWorkflowOrThrow(workflowId);
|
|
401
|
+
const step = workflow.steps.find((s) => s.id === stepId);
|
|
402
|
+
if (!step) {
|
|
403
|
+
throw new WorkflowStepNotFoundError(workflowId, stepId);
|
|
404
|
+
}
|
|
405
|
+
// Record event for orchestrator
|
|
406
|
+
await this.wakeupService.recordEvent({
|
|
407
|
+
workflowId,
|
|
408
|
+
type: "step_started", // Re-use step_started as retry signal
|
|
409
|
+
stepId,
|
|
410
|
+
payload: {
|
|
411
|
+
action: "retry",
|
|
412
|
+
issueId: step.issueId,
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Skip a step.
|
|
418
|
+
*
|
|
419
|
+
* Records an event for the orchestrator to handle.
|
|
420
|
+
* The orchestrator decides how to handle dependents.
|
|
421
|
+
*
|
|
422
|
+
* @param workflowId - The workflow containing the step
|
|
423
|
+
* @param stepId - The step to skip
|
|
424
|
+
* @param reason - Optional reason for skipping
|
|
425
|
+
* @throws WorkflowNotFoundError if workflow doesn't exist
|
|
426
|
+
* @throws WorkflowStepNotFoundError if step doesn't exist
|
|
427
|
+
*/
|
|
428
|
+
async skipStep(workflowId, stepId, reason) {
|
|
429
|
+
const workflow = await this.getWorkflowOrThrow(workflowId);
|
|
430
|
+
const step = workflow.steps.find((s) => s.id === stepId);
|
|
431
|
+
if (!step) {
|
|
432
|
+
throw new WorkflowStepNotFoundError(workflowId, stepId);
|
|
433
|
+
}
|
|
434
|
+
// Record event for orchestrator
|
|
435
|
+
await this.wakeupService.recordEvent({
|
|
436
|
+
workflowId,
|
|
437
|
+
type: "step_skipped",
|
|
438
|
+
stepId,
|
|
439
|
+
payload: {
|
|
440
|
+
action: "skip",
|
|
441
|
+
issueId: step.issueId,
|
|
442
|
+
reason: reason || "Manually skipped",
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
// ===========================================================================
|
|
447
|
+
// Escalation
|
|
448
|
+
// ===========================================================================
|
|
449
|
+
/**
|
|
450
|
+
* Trigger a wakeup for an escalation response.
|
|
451
|
+
*
|
|
452
|
+
* Called by the API when a user responds to an escalation.
|
|
453
|
+
* Immediately triggers the orchestrator to resume with the response.
|
|
454
|
+
*
|
|
455
|
+
* @param workflowId - The workflow to wake up
|
|
456
|
+
*/
|
|
457
|
+
async triggerEscalationWakeup(workflowId) {
|
|
458
|
+
await this.wakeupService.triggerWakeup(workflowId);
|
|
459
|
+
}
|
|
460
|
+
// ===========================================================================
|
|
461
|
+
// Private: Orchestrator Spawning
|
|
462
|
+
// ===========================================================================
|
|
463
|
+
/**
|
|
464
|
+
* Spawn an orchestrator execution (Claude Code agent).
|
|
465
|
+
*
|
|
466
|
+
* The orchestrator is given:
|
|
467
|
+
* - Workflow MCP tools for control
|
|
468
|
+
* - Initial prompt with workflow context
|
|
469
|
+
* - Access to sudocode MCP tools for issue management
|
|
470
|
+
*/
|
|
471
|
+
async spawnOrchestrator(workflow) {
|
|
472
|
+
// Get issues for initial prompt
|
|
473
|
+
const issues = this.getIssuesForWorkflow(workflow);
|
|
474
|
+
// Build initial prompt
|
|
475
|
+
const prompt = this.promptBuilder.buildInitialPrompt(workflow, issues);
|
|
476
|
+
// Build agent config with MCP servers
|
|
477
|
+
const agentConfig = this.buildOrchestratorConfig(workflow);
|
|
478
|
+
// Determine agent type (default to claude-code)
|
|
479
|
+
const agentType = workflow.config.orchestratorAgentType ?? "claude-code";
|
|
480
|
+
// Log the full config being passed to createExecution
|
|
481
|
+
// Orchestrator runs in the workflow's worktree so it can see step changes
|
|
482
|
+
const fullConfig = {
|
|
483
|
+
mode: "worktree",
|
|
484
|
+
baseBranch: workflow.baseBranch,
|
|
485
|
+
reuseWorktreePath: workflow.worktreePath,
|
|
486
|
+
...agentConfig,
|
|
487
|
+
};
|
|
488
|
+
console.log("[OrchestratorWorkflowEngine] Creating execution with config:", JSON.stringify(fullConfig, null, 2));
|
|
489
|
+
// Create orchestrator execution
|
|
490
|
+
const execution = await this.executionService.createExecution(null, // No issue - this is the orchestrator itself
|
|
491
|
+
fullConfig, prompt, agentType);
|
|
492
|
+
// Store orchestrator execution ID and session ID on workflow
|
|
493
|
+
this.updateWorkflow(workflow.id, {
|
|
494
|
+
orchestratorExecutionId: execution.id,
|
|
495
|
+
orchestratorSessionId: execution.session_id,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Build the orchestrator agent configuration.
|
|
500
|
+
*
|
|
501
|
+
* Configures MCP servers for workflow control and sudocode access.
|
|
502
|
+
* Enables dangerouslySkipPermissions for automated workflow execution.
|
|
503
|
+
*/
|
|
504
|
+
buildOrchestratorConfig(workflow) {
|
|
505
|
+
const config = {
|
|
506
|
+
// Orchestrator runs autonomously - must skip permission prompts
|
|
507
|
+
dangerouslySkipPermissions: true,
|
|
508
|
+
};
|
|
509
|
+
// Add workflow MCP server
|
|
510
|
+
// Note: When running in dev mode (tsx/ts-node), __dirname points to src/
|
|
511
|
+
// but we need the compiled dist/ path. Detect and fix this.
|
|
512
|
+
let mcpServerPath = this.config.mcpServerPath;
|
|
513
|
+
if (!mcpServerPath) {
|
|
514
|
+
const defaultPath = path.join(__dirname, "../mcp/index.js");
|
|
515
|
+
// If path contains /src/, replace with /dist/ to get compiled version
|
|
516
|
+
mcpServerPath = defaultPath.includes("/src/")
|
|
517
|
+
? defaultPath.replace("/src/", "/dist/")
|
|
518
|
+
: defaultPath;
|
|
519
|
+
}
|
|
520
|
+
console.log("[OrchestratorWorkflowEngine] MCP server path:", mcpServerPath);
|
|
521
|
+
const mcpArgs = [
|
|
522
|
+
mcpServerPath,
|
|
523
|
+
"--workflow-id",
|
|
524
|
+
workflow.id,
|
|
525
|
+
"--server-url",
|
|
526
|
+
this.config.serverUrl,
|
|
527
|
+
"--project-id",
|
|
528
|
+
this.config.projectId,
|
|
529
|
+
"--repo-path",
|
|
530
|
+
this.config.repoPath,
|
|
531
|
+
];
|
|
532
|
+
config.mcpServers = {
|
|
533
|
+
"sudocode-workflow": {
|
|
534
|
+
command: "node",
|
|
535
|
+
args: mcpArgs,
|
|
536
|
+
},
|
|
537
|
+
};
|
|
538
|
+
// Log the MCP run command for local testing
|
|
539
|
+
console.log("[OrchestratorWorkflowEngine] MCP server config for workflow", workflow.id);
|
|
540
|
+
console.log("[OrchestratorWorkflowEngine] To test MCP locally, run:\n" +
|
|
541
|
+
` node ${mcpArgs.join(" ")}`);
|
|
542
|
+
// Set model if specified
|
|
543
|
+
if (workflow.config.orchestratorModel) {
|
|
544
|
+
config.model = workflow.config.orchestratorModel;
|
|
545
|
+
}
|
|
546
|
+
// Add system prompt extension for orchestrator role
|
|
547
|
+
config.appendSystemPrompt = `
|
|
548
|
+
You are a workflow orchestrator managing the execution of coding tasks.
|
|
549
|
+
You have access to workflow MCP tools to control execution flow.
|
|
550
|
+
|
|
551
|
+
IMPORTANT: You are orchestrating workflow "${workflow.id}".
|
|
552
|
+
Use the workflow tools to:
|
|
553
|
+
- Check workflow status with workflow_status
|
|
554
|
+
- Execute issues with execute_issue
|
|
555
|
+
- Inspect results with execution_trajectory and execution_changes
|
|
556
|
+
- Handle failures appropriately based on the workflow config
|
|
557
|
+
- Mark the workflow complete when done with workflow_complete
|
|
558
|
+
`;
|
|
559
|
+
return config;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Get issues for the workflow (for initial prompt).
|
|
563
|
+
*/
|
|
564
|
+
getIssuesForWorkflow(workflow) {
|
|
565
|
+
const issues = [];
|
|
566
|
+
for (const step of workflow.steps) {
|
|
567
|
+
const issue = getIssue(this.db, step.issueId);
|
|
568
|
+
if (issue) {
|
|
569
|
+
issues.push(issue);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return issues;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Cancel ALL running executions for a workflow.
|
|
576
|
+
* This includes:
|
|
577
|
+
* 1. The entire orchestrator execution chain (root + all follow-ups)
|
|
578
|
+
* 2. All step executions linked to this workflow
|
|
579
|
+
*/
|
|
580
|
+
async cancelAllWorkflowExecutions(workflow) {
|
|
581
|
+
const cancelledIds = new Set();
|
|
582
|
+
// 1. Cancel the entire orchestrator execution chain
|
|
583
|
+
if (workflow.orchestratorExecutionId) {
|
|
584
|
+
// Get the root orchestrator execution by traversing up parent_execution_id
|
|
585
|
+
let rootId = workflow.orchestratorExecutionId;
|
|
586
|
+
let safetyCounter = 100; // Prevent infinite loops
|
|
587
|
+
while (safetyCounter > 0) {
|
|
588
|
+
const parent = this.db
|
|
589
|
+
.prepare("SELECT parent_execution_id FROM executions WHERE id = ?")
|
|
590
|
+
.get(rootId);
|
|
591
|
+
if (!parent || !parent.parent_execution_id) {
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
rootId = parent.parent_execution_id;
|
|
595
|
+
safetyCounter--;
|
|
596
|
+
}
|
|
597
|
+
// Get all executions in the chain (root + all descendants)
|
|
598
|
+
const chainExecutions = this.db
|
|
599
|
+
.prepare(`
|
|
600
|
+
WITH RECURSIVE execution_chain AS (
|
|
601
|
+
-- Base case: the root execution
|
|
602
|
+
SELECT id, status FROM executions WHERE id = ?
|
|
603
|
+
UNION ALL
|
|
604
|
+
-- Recursive case: children of executions in the chain
|
|
605
|
+
SELECT e.id, e.status FROM executions e
|
|
606
|
+
INNER JOIN execution_chain ec ON e.parent_execution_id = ec.id
|
|
607
|
+
)
|
|
608
|
+
SELECT id, status FROM execution_chain
|
|
609
|
+
WHERE status IN ('pending', 'running', 'preparing')
|
|
610
|
+
`)
|
|
611
|
+
.all(rootId);
|
|
612
|
+
console.log(`[OrchestratorEngine] Cancelling ${chainExecutions.length} orchestrator executions in chain`);
|
|
613
|
+
for (const { id } of chainExecutions) {
|
|
614
|
+
if (cancelledIds.has(id))
|
|
615
|
+
continue;
|
|
616
|
+
try {
|
|
617
|
+
await this.executionService.cancelExecution(id);
|
|
618
|
+
cancelledIds.add(id);
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
console.warn(`Failed to cancel orchestrator execution ${id}:`, error);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
// 2. Cancel all step executions linked to this workflow
|
|
626
|
+
const stepExecutions = this.db
|
|
627
|
+
.prepare(`
|
|
628
|
+
SELECT id FROM executions
|
|
629
|
+
WHERE workflow_execution_id = ?
|
|
630
|
+
AND status IN ('pending', 'running', 'preparing')
|
|
631
|
+
`)
|
|
632
|
+
.all(workflow.id);
|
|
633
|
+
console.log(`[OrchestratorEngine] Cancelling ${stepExecutions.length} step executions`);
|
|
634
|
+
for (const { id } of stepExecutions) {
|
|
635
|
+
if (cancelledIds.has(id))
|
|
636
|
+
continue;
|
|
637
|
+
try {
|
|
638
|
+
await this.executionService.cancelExecution(id);
|
|
639
|
+
cancelledIds.add(id);
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
console.warn(`Failed to cancel step execution ${id}:`, error);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
console.log(`[OrchestratorEngine] Cancelled ${cancelledIds.size} total executions for workflow ${workflow.id}`);
|
|
646
|
+
}
|
|
647
|
+
// ===========================================================================
|
|
648
|
+
// Recovery
|
|
649
|
+
// ===========================================================================
|
|
650
|
+
/**
|
|
651
|
+
* Recover orphaned workflows on server restart.
|
|
652
|
+
*
|
|
653
|
+
* Finds workflows in 'running' status whose orchestrator execution
|
|
654
|
+
* is no longer running, and triggers a wakeup to resume them.
|
|
655
|
+
*/
|
|
656
|
+
async recoverOrphanedWorkflows() {
|
|
657
|
+
console.log("[OrchestratorEngine] Checking for orphaned workflows...");
|
|
658
|
+
// Find workflows in 'running' status
|
|
659
|
+
const runningWorkflows = this.db
|
|
660
|
+
.prepare(`SELECT * FROM workflows WHERE status = 'running'`)
|
|
661
|
+
.all();
|
|
662
|
+
let recoveredCount = 0;
|
|
663
|
+
for (const row of runningWorkflows) {
|
|
664
|
+
// Parse the workflow row
|
|
665
|
+
const workflow = {
|
|
666
|
+
id: row.id,
|
|
667
|
+
title: row.title,
|
|
668
|
+
source: JSON.parse(row.source),
|
|
669
|
+
status: row.status,
|
|
670
|
+
steps: JSON.parse(row.steps),
|
|
671
|
+
worktreePath: row.worktree_path ?? undefined,
|
|
672
|
+
branchName: row.branch_name ?? undefined,
|
|
673
|
+
baseBranch: row.base_branch,
|
|
674
|
+
currentStepIndex: row.current_step_index,
|
|
675
|
+
orchestratorExecutionId: row.orchestrator_execution_id ?? undefined,
|
|
676
|
+
orchestratorSessionId: row.orchestrator_session_id ?? undefined,
|
|
677
|
+
config: JSON.parse(row.config),
|
|
678
|
+
createdAt: row.created_at,
|
|
679
|
+
updatedAt: row.updated_at,
|
|
680
|
+
startedAt: row.started_at ?? undefined,
|
|
681
|
+
completedAt: row.completed_at ?? undefined,
|
|
682
|
+
};
|
|
683
|
+
// Skip if no orchestrator execution
|
|
684
|
+
if (!workflow.orchestratorExecutionId) {
|
|
685
|
+
console.warn(`[OrchestratorEngine] Workflow ${workflow.id} running but no orchestrator`);
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
// Check orchestrator execution status
|
|
689
|
+
const orchestrator = this.db
|
|
690
|
+
.prepare(`SELECT status FROM executions WHERE id = ?`)
|
|
691
|
+
.get(workflow.orchestratorExecutionId);
|
|
692
|
+
// If orchestrator is not running, trigger recovery
|
|
693
|
+
if (!orchestrator || orchestrator.status !== "running") {
|
|
694
|
+
console.log(`[OrchestratorEngine] Recovering workflow ${workflow.id} ` +
|
|
695
|
+
`(orchestrator status: ${orchestrator?.status ?? "not found"})`);
|
|
696
|
+
try {
|
|
697
|
+
// Record recovery event
|
|
698
|
+
await this.wakeupService.recordEvent({
|
|
699
|
+
workflowId: workflow.id,
|
|
700
|
+
type: "orchestrator_wakeup",
|
|
701
|
+
payload: {
|
|
702
|
+
reason: "recovery",
|
|
703
|
+
previousStatus: orchestrator?.status,
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
// Trigger wakeup to resume
|
|
707
|
+
await this.wakeupService.triggerWakeup(workflow.id);
|
|
708
|
+
recoveredCount++;
|
|
709
|
+
}
|
|
710
|
+
catch (err) {
|
|
711
|
+
console.error(`[OrchestratorEngine] Failed to recover workflow ${workflow.id}:`, err);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
console.log(`[OrchestratorEngine] Recovery complete: ${recoveredCount}/${runningWorkflows.length} workflows recovered`);
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Mark stale running executions as failed.
|
|
719
|
+
*
|
|
720
|
+
* Called during recovery to clean up executions that were running
|
|
721
|
+
* when the server crashed.
|
|
722
|
+
*/
|
|
723
|
+
async markStaleExecutionsAsFailed() {
|
|
724
|
+
console.log("[OrchestratorEngine] Checking for stale executions...");
|
|
725
|
+
const staleExecutions = this.db
|
|
726
|
+
.prepare(`
|
|
727
|
+
SELECT id, workflow_execution_id FROM executions
|
|
728
|
+
WHERE status = 'running'
|
|
729
|
+
AND workflow_execution_id IS NOT NULL
|
|
730
|
+
`)
|
|
731
|
+
.all();
|
|
732
|
+
for (const exec of staleExecutions) {
|
|
733
|
+
console.log(`[OrchestratorEngine] Marking stale execution ${exec.id} as failed`);
|
|
734
|
+
this.db
|
|
735
|
+
.prepare(`
|
|
736
|
+
UPDATE executions
|
|
737
|
+
SET status = 'failed',
|
|
738
|
+
error_message = 'Execution was running when server restarted',
|
|
739
|
+
completed_at = ?
|
|
740
|
+
WHERE id = ?
|
|
741
|
+
`)
|
|
742
|
+
.run(new Date().toISOString(), exec.id);
|
|
743
|
+
}
|
|
744
|
+
if (staleExecutions.length > 0) {
|
|
745
|
+
console.log(`[OrchestratorEngine] Marked ${staleExecutions.length} stale executions as failed`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
//# sourceMappingURL=orchestrator-engine.js.map
|