@sudocode-ai/local-server 0.1.10 → 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/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/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/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 +36 -0
- package/dist/services/execution-lifecycle.d.ts.map +1 -1
- package/dist/services/execution-lifecycle.js +87 -0
- 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 +90 -1
- 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/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-CQoCSnhl.css +0 -1
- package/dist/public/assets/index-iWE3gSYw.js +0 -758
- package/dist/public/assets/index-iWE3gSYw.js.map +0 -1
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkflowWakeupService
|
|
3
|
+
*
|
|
4
|
+
* Handles event recording and orchestrator wakeups.
|
|
5
|
+
* Events are batched within a configurable window before triggering wakeups.
|
|
6
|
+
*/
|
|
7
|
+
import { randomUUID } from "crypto";
|
|
8
|
+
/**
|
|
9
|
+
* Default wakeup configuration
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_WAKEUP_CONFIG = {
|
|
12
|
+
batchWindowMs: 5000, // 5 seconds
|
|
13
|
+
};
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Helper Functions
|
|
16
|
+
// =============================================================================
|
|
17
|
+
/**
|
|
18
|
+
* Convert a database row to a WorkflowEvent object
|
|
19
|
+
*/
|
|
20
|
+
function rowToEvent(row) {
|
|
21
|
+
return {
|
|
22
|
+
id: row.id,
|
|
23
|
+
workflowId: row.workflow_id,
|
|
24
|
+
type: row.type,
|
|
25
|
+
stepId: row.step_id ?? undefined,
|
|
26
|
+
executionId: row.execution_id ?? undefined,
|
|
27
|
+
payload: JSON.parse(row.payload),
|
|
28
|
+
createdAt: row.created_at,
|
|
29
|
+
processedAt: row.processed_at ?? undefined,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// WorkflowWakeupService
|
|
34
|
+
// =============================================================================
|
|
35
|
+
/**
|
|
36
|
+
* Service for recording workflow events and triggering orchestrator wakeups.
|
|
37
|
+
*
|
|
38
|
+
* Events are batched within a configurable window. When the window expires,
|
|
39
|
+
* a wakeup is triggered by creating a follow-up execution for the orchestrator.
|
|
40
|
+
*/
|
|
41
|
+
export class WorkflowWakeupService {
|
|
42
|
+
db;
|
|
43
|
+
executionService;
|
|
44
|
+
promptBuilder;
|
|
45
|
+
eventEmitter;
|
|
46
|
+
config;
|
|
47
|
+
/** Pending wakeup timers by workflow ID */
|
|
48
|
+
pendingWakeups = new Map();
|
|
49
|
+
/** Active execution timeout timers by execution ID */
|
|
50
|
+
executionTimeouts = new Map();
|
|
51
|
+
/** Pending await conditions by workflow ID */
|
|
52
|
+
pendingAwaits = new Map();
|
|
53
|
+
/** Await timeout timers by await ID */
|
|
54
|
+
awaitTimeouts = new Map();
|
|
55
|
+
/** Last resolved await for each workflow (for wakeup message context) */
|
|
56
|
+
resolvedAwaits = new Map();
|
|
57
|
+
/** Whether the service is running (for timeout monitoring) */
|
|
58
|
+
_isRunning = false;
|
|
59
|
+
/** Check if the service is running */
|
|
60
|
+
get isRunning() {
|
|
61
|
+
return this._isRunning;
|
|
62
|
+
}
|
|
63
|
+
constructor(deps) {
|
|
64
|
+
this.db = deps.db;
|
|
65
|
+
this.executionService = deps.executionService;
|
|
66
|
+
this.promptBuilder = deps.promptBuilder;
|
|
67
|
+
this.eventEmitter = deps.eventEmitter;
|
|
68
|
+
this.config = { ...DEFAULT_WAKEUP_CONFIG, ...deps.config };
|
|
69
|
+
}
|
|
70
|
+
// ===========================================================================
|
|
71
|
+
// Event Recording
|
|
72
|
+
// ===========================================================================
|
|
73
|
+
/**
|
|
74
|
+
* Record a workflow event.
|
|
75
|
+
* Automatically schedules a wakeup after the batch window.
|
|
76
|
+
* If an await condition is satisfied, triggers immediate wakeup.
|
|
77
|
+
*/
|
|
78
|
+
async recordEvent(params) {
|
|
79
|
+
const eventId = randomUUID();
|
|
80
|
+
const now = new Date().toISOString();
|
|
81
|
+
// Insert event into database
|
|
82
|
+
this.db
|
|
83
|
+
.prepare(`
|
|
84
|
+
INSERT INTO workflow_events (id, workflow_id, type, step_id, execution_id, payload, created_at)
|
|
85
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
86
|
+
`)
|
|
87
|
+
.run(eventId, params.workflowId, params.type, params.stepId ?? null, params.executionId ?? null, JSON.stringify(params.payload), now);
|
|
88
|
+
// Check if this event satisfies a pending await condition
|
|
89
|
+
const event = {
|
|
90
|
+
id: eventId,
|
|
91
|
+
workflowId: params.workflowId,
|
|
92
|
+
type: params.type,
|
|
93
|
+
stepId: params.stepId,
|
|
94
|
+
executionId: params.executionId,
|
|
95
|
+
payload: params.payload,
|
|
96
|
+
createdAt: now,
|
|
97
|
+
};
|
|
98
|
+
if (this.checkAwaitCondition(params.workflowId, event)) {
|
|
99
|
+
// Resolve the await - this stores context for wakeup message
|
|
100
|
+
this.resolveAwait(params.workflowId, params.type);
|
|
101
|
+
// Trigger immediate wakeup (skip debounce)
|
|
102
|
+
await this.triggerWakeup(params.workflowId);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Schedule wakeup (debounced)
|
|
106
|
+
this.scheduleWakeup(params.workflowId);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Get all unprocessed events for a workflow.
|
|
110
|
+
*/
|
|
111
|
+
getUnprocessedEvents(workflowId) {
|
|
112
|
+
const rows = this.db
|
|
113
|
+
.prepare(`
|
|
114
|
+
SELECT * FROM workflow_events
|
|
115
|
+
WHERE workflow_id = ? AND processed_at IS NULL
|
|
116
|
+
ORDER BY created_at ASC
|
|
117
|
+
`)
|
|
118
|
+
.all(workflowId);
|
|
119
|
+
return rows.map(rowToEvent);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Mark events as processed.
|
|
123
|
+
*/
|
|
124
|
+
markEventsProcessed(eventIds) {
|
|
125
|
+
if (eventIds.length === 0)
|
|
126
|
+
return;
|
|
127
|
+
const now = new Date().toISOString();
|
|
128
|
+
const placeholders = eventIds.map(() => "?").join(", ");
|
|
129
|
+
this.db
|
|
130
|
+
.prepare(`UPDATE workflow_events SET processed_at = ? WHERE id IN (${placeholders})`)
|
|
131
|
+
.run(now, ...eventIds);
|
|
132
|
+
}
|
|
133
|
+
// ===========================================================================
|
|
134
|
+
// Wakeup Scheduling
|
|
135
|
+
// ===========================================================================
|
|
136
|
+
/**
|
|
137
|
+
* Schedule a wakeup for a workflow.
|
|
138
|
+
* Debounced within the batch window - multiple events will be batched.
|
|
139
|
+
*/
|
|
140
|
+
scheduleWakeup(workflowId) {
|
|
141
|
+
// Cancel any existing pending wakeup
|
|
142
|
+
const existing = this.pendingWakeups.get(workflowId);
|
|
143
|
+
if (existing) {
|
|
144
|
+
clearTimeout(existing);
|
|
145
|
+
}
|
|
146
|
+
// Schedule new wakeup after batch window
|
|
147
|
+
const timeout = setTimeout(() => {
|
|
148
|
+
this.pendingWakeups.delete(workflowId);
|
|
149
|
+
this.triggerWakeup(workflowId).catch((err) => {
|
|
150
|
+
console.error(`Failed to trigger wakeup for workflow ${workflowId}:`, err);
|
|
151
|
+
});
|
|
152
|
+
}, this.config.batchWindowMs);
|
|
153
|
+
this.pendingWakeups.set(workflowId, timeout);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Cancel a pending wakeup for a workflow.
|
|
157
|
+
*/
|
|
158
|
+
cancelPendingWakeup(workflowId) {
|
|
159
|
+
const existing = this.pendingWakeups.get(workflowId);
|
|
160
|
+
if (existing) {
|
|
161
|
+
clearTimeout(existing);
|
|
162
|
+
this.pendingWakeups.delete(workflowId);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// ===========================================================================
|
|
166
|
+
// Execution Timeout Tracking
|
|
167
|
+
// ===========================================================================
|
|
168
|
+
/**
|
|
169
|
+
* Start a timeout for an execution.
|
|
170
|
+
*
|
|
171
|
+
* When the timeout fires, the execution is cancelled and a step_failed
|
|
172
|
+
* event is recorded with reason "timeout".
|
|
173
|
+
*
|
|
174
|
+
* The timeout deadline is persisted to the database so it can be recovered
|
|
175
|
+
* after server restart.
|
|
176
|
+
*
|
|
177
|
+
* @param executionId - The execution to track
|
|
178
|
+
* @param workflowId - The workflow the execution belongs to
|
|
179
|
+
* @param stepId - The workflow step ID
|
|
180
|
+
* @param timeoutMs - Timeout duration in milliseconds
|
|
181
|
+
*/
|
|
182
|
+
startExecutionTimeout(executionId, workflowId, stepId, timeoutMs) {
|
|
183
|
+
// Clear any existing timeout for this execution
|
|
184
|
+
this.clearExecutionTimeout(executionId);
|
|
185
|
+
// Calculate absolute timeout deadline
|
|
186
|
+
const timeoutAt = new Date(Date.now() + timeoutMs).toISOString();
|
|
187
|
+
// Persist the timeout to the database for recovery after restart
|
|
188
|
+
// Use INSERT OR REPLACE to handle replacing an existing timeout for the same execution
|
|
189
|
+
const now = new Date().toISOString();
|
|
190
|
+
this.db
|
|
191
|
+
.prepare(`
|
|
192
|
+
INSERT OR REPLACE INTO workflow_events (id, workflow_id, type, execution_id, step_id, payload, created_at)
|
|
193
|
+
VALUES (?, ?, 'execution_timeout', ?, ?, ?, ?)
|
|
194
|
+
`)
|
|
195
|
+
.run(`timeout-${executionId}`, workflowId, executionId, stepId, JSON.stringify({ timeoutAt }), now);
|
|
196
|
+
const timeout = setTimeout(() => {
|
|
197
|
+
this.executionTimeouts.delete(executionId);
|
|
198
|
+
this.handleExecutionTimeout(executionId, workflowId, stepId).catch((err) => {
|
|
199
|
+
console.error(`[WakeupService] Error handling execution timeout:`, err);
|
|
200
|
+
});
|
|
201
|
+
}, timeoutMs);
|
|
202
|
+
this.executionTimeouts.set(executionId, { timeout, workflowId, stepId });
|
|
203
|
+
console.log(`[WakeupService] Started ${timeoutMs}ms timeout for execution ${executionId} (deadline: ${timeoutAt})`);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Clear timeout for an execution.
|
|
207
|
+
*
|
|
208
|
+
* Call this when an execution completes normally to prevent
|
|
209
|
+
* the timeout from firing. Also marks the persisted timeout
|
|
210
|
+
* event as processed in the database.
|
|
211
|
+
*
|
|
212
|
+
* @param executionId - The execution to clear timeout for
|
|
213
|
+
*/
|
|
214
|
+
clearExecutionTimeout(executionId) {
|
|
215
|
+
const entry = this.executionTimeouts.get(executionId);
|
|
216
|
+
if (entry) {
|
|
217
|
+
clearTimeout(entry.timeout);
|
|
218
|
+
this.executionTimeouts.delete(executionId);
|
|
219
|
+
}
|
|
220
|
+
// Mark the persisted timeout event as processed
|
|
221
|
+
const result = this.db
|
|
222
|
+
.prepare(`
|
|
223
|
+
UPDATE workflow_events
|
|
224
|
+
SET processed_at = ?
|
|
225
|
+
WHERE id = ? AND type = 'execution_timeout' AND processed_at IS NULL
|
|
226
|
+
`)
|
|
227
|
+
.run(new Date().toISOString(), `timeout-${executionId}`);
|
|
228
|
+
if (entry || result.changes > 0) {
|
|
229
|
+
console.log(`[WakeupService] Cleared timeout for execution ${executionId}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Handle an execution timeout.
|
|
234
|
+
*
|
|
235
|
+
* Cancels the execution and records a step_failed event.
|
|
236
|
+
*
|
|
237
|
+
* @param executionId - The timed-out execution
|
|
238
|
+
* @param workflowId - The workflow containing the execution
|
|
239
|
+
* @param stepId - The workflow step that timed out
|
|
240
|
+
*/
|
|
241
|
+
async handleExecutionTimeout(executionId, workflowId, stepId) {
|
|
242
|
+
console.warn(`[WakeupService] Execution ${executionId} timed out`);
|
|
243
|
+
// Mark the persisted timeout event as processed
|
|
244
|
+
this.db
|
|
245
|
+
.prepare(`
|
|
246
|
+
UPDATE workflow_events
|
|
247
|
+
SET processed_at = ?
|
|
248
|
+
WHERE id = ? AND type = 'execution_timeout' AND processed_at IS NULL
|
|
249
|
+
`)
|
|
250
|
+
.run(new Date().toISOString(), `timeout-${executionId}`);
|
|
251
|
+
// Try to cancel the execution
|
|
252
|
+
try {
|
|
253
|
+
await this.executionService.cancelExecution(executionId);
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
console.error(`[WakeupService] Failed to cancel timed-out execution ${executionId}:`, err);
|
|
257
|
+
// Continue to record the timeout event anyway
|
|
258
|
+
}
|
|
259
|
+
// Record timeout event (will trigger wakeup via scheduleWakeup)
|
|
260
|
+
await this.recordEvent({
|
|
261
|
+
workflowId,
|
|
262
|
+
type: "step_failed",
|
|
263
|
+
executionId,
|
|
264
|
+
stepId,
|
|
265
|
+
payload: {
|
|
266
|
+
reason: "timeout",
|
|
267
|
+
message: "Execution exceeded configured timeout",
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
// ===========================================================================
|
|
272
|
+
// Wakeup Triggering
|
|
273
|
+
// ===========================================================================
|
|
274
|
+
/**
|
|
275
|
+
* Trigger an immediate wakeup for a workflow.
|
|
276
|
+
* Collects unprocessed events, builds a wakeup message, and creates
|
|
277
|
+
* a follow-up execution for the orchestrator.
|
|
278
|
+
*/
|
|
279
|
+
async triggerWakeup(workflowId) {
|
|
280
|
+
// 1. Get workflow
|
|
281
|
+
const workflow = this.getWorkflow(workflowId);
|
|
282
|
+
if (!workflow) {
|
|
283
|
+
console.warn(`Cannot trigger wakeup: workflow ${workflowId} not found`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
// 2. Check if workflow has an orchestrator execution
|
|
287
|
+
if (!workflow.orchestratorExecutionId) {
|
|
288
|
+
console.warn(`Cannot trigger wakeup: workflow ${workflowId} has no orchestrator execution`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
// 3. Check workflow status - don't wake if paused/cancelled/completed
|
|
292
|
+
if (workflow.status === "paused" ||
|
|
293
|
+
workflow.status === "cancelled" ||
|
|
294
|
+
workflow.status === "completed" ||
|
|
295
|
+
workflow.status === "failed") {
|
|
296
|
+
console.debug(`Skipping wakeup for workflow ${workflowId}: status is ${workflow.status}`);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
// 4. Get unprocessed events
|
|
300
|
+
const events = this.getUnprocessedEvents(workflowId);
|
|
301
|
+
// 5. Get resolved await context (if any) - check before events check
|
|
302
|
+
// This allows timeout wakeups to proceed even with no events
|
|
303
|
+
const resolvedAwait = this.getAndClearResolvedAwait(workflowId);
|
|
304
|
+
// 6. Skip if no events AND no resolved await (nothing to report)
|
|
305
|
+
if (events.length === 0 && !resolvedAwait) {
|
|
306
|
+
console.debug(`No unprocessed events for workflow ${workflowId}`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
// 7. Get executions referenced by events
|
|
310
|
+
const executions = this.getExecutionsForEvents(events);
|
|
311
|
+
// 8. Build wakeup message with await context
|
|
312
|
+
const message = this.promptBuilder.buildWakeupMessage(events, executions, resolvedAwait);
|
|
313
|
+
// 9. Stop the previous orchestrator execution before creating follow-up
|
|
314
|
+
// This prevents parallel execution of orchestrator and follow-up
|
|
315
|
+
// TODO: Migrate to using agent executor 'sendMessage' for inter-execution messages.
|
|
316
|
+
try {
|
|
317
|
+
const prevExecution = this.db
|
|
318
|
+
.prepare("SELECT status FROM executions WHERE id = ?")
|
|
319
|
+
.get(workflow.orchestratorExecutionId);
|
|
320
|
+
if (prevExecution &&
|
|
321
|
+
["running", "pending", "preparing"].includes(prevExecution.status)) {
|
|
322
|
+
console.log(`[WakeupService] Stopping previous orchestrator execution ${workflow.orchestratorExecutionId} before creating follow-up`);
|
|
323
|
+
await this.executionService.cancelExecution(workflow.orchestratorExecutionId);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
console.warn(`[WakeupService] Failed to stop previous orchestrator execution ${workflow.orchestratorExecutionId}:`, err);
|
|
328
|
+
// Continue with follow-up creation anyway
|
|
329
|
+
}
|
|
330
|
+
// 10. Create follow-up execution
|
|
331
|
+
try {
|
|
332
|
+
const followUp = await this.executionService.createFollowUp(workflow.orchestratorExecutionId, message);
|
|
333
|
+
// 11. Update workflow with new orchestrator execution ID
|
|
334
|
+
this.updateOrchestratorExecution(workflowId, followUp.id, followUp.session_id);
|
|
335
|
+
// 12. Mark events as processed
|
|
336
|
+
this.markEventsProcessed(events.map((e) => e.id));
|
|
337
|
+
// 13. Emit wakeup event
|
|
338
|
+
this.eventEmitter.emit({
|
|
339
|
+
type: "orchestrator_wakeup",
|
|
340
|
+
workflowId,
|
|
341
|
+
payload: {
|
|
342
|
+
eventCount: events.length,
|
|
343
|
+
executionId: followUp.id,
|
|
344
|
+
},
|
|
345
|
+
timestamp: Date.now(),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
console.error(`Failed to create follow-up execution for workflow ${workflowId}:`, err);
|
|
350
|
+
// Don't mark events as processed - they'll be retried on next wakeup
|
|
351
|
+
throw err;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// ===========================================================================
|
|
355
|
+
// Lifecycle
|
|
356
|
+
// ===========================================================================
|
|
357
|
+
/**
|
|
358
|
+
* Start the wakeup service (enables timeout monitoring).
|
|
359
|
+
*/
|
|
360
|
+
start() {
|
|
361
|
+
this._isRunning = true;
|
|
362
|
+
// TODO: Implement idle timeout monitoring if configured
|
|
363
|
+
// TODO: Implement execution timeout monitoring if configured
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Recover state from the database after server restart.
|
|
367
|
+
*
|
|
368
|
+
* This method:
|
|
369
|
+
* 1. Recovers pending await conditions from workflow_events
|
|
370
|
+
* 2. Reschedules await timeouts (calculating remaining time)
|
|
371
|
+
* 3. Triggers immediate wakeup for expired awaits
|
|
372
|
+
* 4. Schedules wakeups for workflows with unprocessed events
|
|
373
|
+
* 5. Recovers execution timeout timers
|
|
374
|
+
*
|
|
375
|
+
* Should be called during server startup after the service is constructed.
|
|
376
|
+
*/
|
|
377
|
+
async recoverState() {
|
|
378
|
+
console.log("[WakeupService] Starting state recovery...");
|
|
379
|
+
// 1. Recover pending await conditions
|
|
380
|
+
await this.recoverPendingAwaits();
|
|
381
|
+
// 2. Schedule wakeups for workflows with unprocessed events
|
|
382
|
+
await this.recoverPendingWakeups();
|
|
383
|
+
// 3. Recover execution timeouts
|
|
384
|
+
await this.recoverExecutionTimeouts();
|
|
385
|
+
console.log("[WakeupService] State recovery complete");
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Recover pending await conditions from the database.
|
|
389
|
+
*/
|
|
390
|
+
async recoverPendingAwaits() {
|
|
391
|
+
// Find unprocessed await events
|
|
392
|
+
const awaitRows = this.db
|
|
393
|
+
.prepare(`
|
|
394
|
+
SELECT we.*, w.status as workflow_status
|
|
395
|
+
FROM workflow_events we
|
|
396
|
+
JOIN workflows w ON we.workflow_id = w.id
|
|
397
|
+
WHERE we.type = 'orchestrator_wakeup'
|
|
398
|
+
AND we.processed_at IS NULL
|
|
399
|
+
AND json_extract(we.payload, '$.awaitType') = 'pending'
|
|
400
|
+
AND w.status = 'running'
|
|
401
|
+
`)
|
|
402
|
+
.all();
|
|
403
|
+
console.log(`[WakeupService] Found ${awaitRows.length} pending awaits to recover`);
|
|
404
|
+
for (const row of awaitRows) {
|
|
405
|
+
try {
|
|
406
|
+
const payload = JSON.parse(row.payload);
|
|
407
|
+
// Reconstruct pendingAwaits Map entry
|
|
408
|
+
const pendingAwait = {
|
|
409
|
+
id: row.id,
|
|
410
|
+
workflowId: row.workflow_id,
|
|
411
|
+
eventTypes: payload.eventTypes,
|
|
412
|
+
executionIds: payload.executionIds,
|
|
413
|
+
timeoutAt: payload.timeoutAt,
|
|
414
|
+
message: payload.message,
|
|
415
|
+
createdAt: row.created_at,
|
|
416
|
+
};
|
|
417
|
+
this.pendingAwaits.set(row.workflow_id, pendingAwait);
|
|
418
|
+
// Handle timeout
|
|
419
|
+
if (payload.timeoutAt) {
|
|
420
|
+
const remainingMs = new Date(payload.timeoutAt).getTime() - Date.now();
|
|
421
|
+
if (remainingMs > 0) {
|
|
422
|
+
// Reschedule timeout with remaining time
|
|
423
|
+
this.scheduleAwaitTimeout(row.workflow_id, row.id, remainingMs / 1000);
|
|
424
|
+
console.log(`[WakeupService] Rescheduled await ${row.id} timeout (${Math.round(remainingMs / 1000)}s remaining)`);
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
// Already expired - trigger immediately
|
|
428
|
+
console.log(`[WakeupService] Await ${row.id} expired during downtime, triggering wakeup`);
|
|
429
|
+
this.resolveAwait(row.workflow_id, "timeout");
|
|
430
|
+
await this.triggerWakeup(row.workflow_id);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
console.log(`[WakeupService] Recovered await ${row.id} (no timeout)`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
console.error(`[WakeupService] Failed to recover await for workflow ${row.workflow_id}:`, error);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Schedule wakeups for workflows with unprocessed events.
|
|
444
|
+
*/
|
|
445
|
+
async recoverPendingWakeups() {
|
|
446
|
+
// Find workflows with unprocessed events (excluding await events and execution timeouts
|
|
447
|
+
// which are handled separately)
|
|
448
|
+
const workflowRows = this.db
|
|
449
|
+
.prepare(`
|
|
450
|
+
SELECT DISTINCT we.workflow_id
|
|
451
|
+
FROM workflow_events we
|
|
452
|
+
JOIN workflows w ON we.workflow_id = w.id
|
|
453
|
+
WHERE we.processed_at IS NULL
|
|
454
|
+
AND we.type NOT IN ('orchestrator_wakeup', 'execution_timeout')
|
|
455
|
+
AND w.status = 'running'
|
|
456
|
+
`)
|
|
457
|
+
.all();
|
|
458
|
+
console.log(`[WakeupService] Found ${workflowRows.length} workflows with unprocessed events`);
|
|
459
|
+
for (const row of workflowRows) {
|
|
460
|
+
// Schedule a wakeup for this workflow
|
|
461
|
+
this.scheduleWakeup(row.workflow_id);
|
|
462
|
+
console.log(`[WakeupService] Scheduled wakeup for workflow ${row.workflow_id}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Recover execution timeout timers from the database.
|
|
467
|
+
*
|
|
468
|
+
* Finds unprocessed execution_timeout events and reschedules the timers
|
|
469
|
+
* with the remaining time. If the timeout has already expired, handles
|
|
470
|
+
* the timeout immediately.
|
|
471
|
+
*/
|
|
472
|
+
async recoverExecutionTimeouts() {
|
|
473
|
+
// Find unprocessed execution timeout events for running executions
|
|
474
|
+
const timeoutRows = this.db
|
|
475
|
+
.prepare(`
|
|
476
|
+
SELECT we.*, e.status as execution_status
|
|
477
|
+
FROM workflow_events we
|
|
478
|
+
JOIN executions e ON we.execution_id = e.id
|
|
479
|
+
JOIN workflows w ON we.workflow_id = w.id
|
|
480
|
+
WHERE we.type = 'execution_timeout'
|
|
481
|
+
AND we.processed_at IS NULL
|
|
482
|
+
AND e.status = 'running'
|
|
483
|
+
AND w.status = 'running'
|
|
484
|
+
`)
|
|
485
|
+
.all();
|
|
486
|
+
console.log(`[WakeupService] Found ${timeoutRows.length} execution timeouts to recover`);
|
|
487
|
+
for (const row of timeoutRows) {
|
|
488
|
+
try {
|
|
489
|
+
const payload = JSON.parse(row.payload);
|
|
490
|
+
const remainingMs = new Date(payload.timeoutAt).getTime() - Date.now();
|
|
491
|
+
if (remainingMs > 0) {
|
|
492
|
+
// Reschedule timeout with remaining time
|
|
493
|
+
const timeout = setTimeout(() => {
|
|
494
|
+
this.executionTimeouts.delete(row.execution_id);
|
|
495
|
+
this.handleExecutionTimeout(row.execution_id, row.workflow_id, row.step_id).catch((err) => {
|
|
496
|
+
console.error(`[WakeupService] Error handling recovered execution timeout:`, err);
|
|
497
|
+
});
|
|
498
|
+
}, remainingMs);
|
|
499
|
+
this.executionTimeouts.set(row.execution_id, {
|
|
500
|
+
timeout,
|
|
501
|
+
workflowId: row.workflow_id,
|
|
502
|
+
stepId: row.step_id,
|
|
503
|
+
});
|
|
504
|
+
console.log(`[WakeupService] Rescheduled execution ${row.execution_id} timeout (${Math.round(remainingMs / 1000)}s remaining)`);
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
// Already expired - handle immediately
|
|
508
|
+
console.log(`[WakeupService] Execution ${row.execution_id} timeout expired during downtime, handling now`);
|
|
509
|
+
await this.handleExecutionTimeout(row.execution_id, row.workflow_id, row.step_id);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
catch (error) {
|
|
513
|
+
console.error(`[WakeupService] Failed to recover timeout for execution ${row.execution_id}:`, error);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Stop the wakeup service.
|
|
519
|
+
* Cancels all pending wakeups, execution timeouts, and await conditions.
|
|
520
|
+
*/
|
|
521
|
+
stop() {
|
|
522
|
+
this._isRunning = false;
|
|
523
|
+
// Cancel all pending wakeups
|
|
524
|
+
for (const [, timeout] of this.pendingWakeups) {
|
|
525
|
+
clearTimeout(timeout);
|
|
526
|
+
}
|
|
527
|
+
this.pendingWakeups.clear();
|
|
528
|
+
// Cancel all execution timeouts
|
|
529
|
+
for (const [, entry] of this.executionTimeouts) {
|
|
530
|
+
clearTimeout(entry.timeout);
|
|
531
|
+
}
|
|
532
|
+
this.executionTimeouts.clear();
|
|
533
|
+
// Cancel all await timeouts
|
|
534
|
+
for (const [, timeout] of this.awaitTimeouts) {
|
|
535
|
+
clearTimeout(timeout);
|
|
536
|
+
}
|
|
537
|
+
this.awaitTimeouts.clear();
|
|
538
|
+
this.pendingAwaits.clear();
|
|
539
|
+
this.resolvedAwaits.clear();
|
|
540
|
+
}
|
|
541
|
+
// ===========================================================================
|
|
542
|
+
// Await Condition Management
|
|
543
|
+
// ===========================================================================
|
|
544
|
+
/**
|
|
545
|
+
* Register a new await condition.
|
|
546
|
+
* Called by the await-events API endpoint.
|
|
547
|
+
*
|
|
548
|
+
* Persists the await to workflow_events for recovery after server restart.
|
|
549
|
+
*/
|
|
550
|
+
registerAwait(params) {
|
|
551
|
+
const awaitId = randomUUID();
|
|
552
|
+
const now = new Date().toISOString();
|
|
553
|
+
const timeoutAt = params.timeoutSeconds
|
|
554
|
+
? new Date(Date.now() + params.timeoutSeconds * 1000).toISOString()
|
|
555
|
+
: undefined;
|
|
556
|
+
const pendingAwait = {
|
|
557
|
+
id: awaitId,
|
|
558
|
+
workflowId: params.workflowId,
|
|
559
|
+
eventTypes: params.eventTypes,
|
|
560
|
+
executionIds: params.executionIds,
|
|
561
|
+
timeoutAt,
|
|
562
|
+
message: params.message,
|
|
563
|
+
createdAt: now,
|
|
564
|
+
};
|
|
565
|
+
// Store in memory (replaces any existing await for this workflow)
|
|
566
|
+
this.pendingAwaits.set(params.workflowId, pendingAwait);
|
|
567
|
+
// Persist to workflow_events for recovery
|
|
568
|
+
// Using 'orchestrator_wakeup' type with awaitType in payload
|
|
569
|
+
this.db
|
|
570
|
+
.prepare(`
|
|
571
|
+
INSERT INTO workflow_events (id, workflow_id, type, payload, created_at)
|
|
572
|
+
VALUES (?, ?, 'orchestrator_wakeup', ?, ?)
|
|
573
|
+
`)
|
|
574
|
+
.run(awaitId, params.workflowId, JSON.stringify({
|
|
575
|
+
awaitType: "pending",
|
|
576
|
+
eventTypes: params.eventTypes,
|
|
577
|
+
executionIds: params.executionIds,
|
|
578
|
+
timeoutAt,
|
|
579
|
+
message: params.message,
|
|
580
|
+
}), now);
|
|
581
|
+
// Schedule timeout if specified
|
|
582
|
+
if (params.timeoutSeconds) {
|
|
583
|
+
this.scheduleAwaitTimeout(params.workflowId, awaitId, params.timeoutSeconds);
|
|
584
|
+
}
|
|
585
|
+
console.log(`[WakeupService] Registered await ${awaitId} for workflow ${params.workflowId}`, { eventTypes: params.eventTypes, executionIds: params.executionIds });
|
|
586
|
+
return { id: awaitId, timeoutAt };
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Check if an event satisfies the pending await for a workflow.
|
|
590
|
+
* Called internally when recording events.
|
|
591
|
+
*/
|
|
592
|
+
checkAwaitCondition(workflowId, event) {
|
|
593
|
+
const pendingAwait = this.pendingAwaits.get(workflowId);
|
|
594
|
+
if (!pendingAwait)
|
|
595
|
+
return false;
|
|
596
|
+
// Map workflow event types to awaitable event types
|
|
597
|
+
const eventType = this.mapToAwaitableEventType(event.type);
|
|
598
|
+
if (!eventType)
|
|
599
|
+
return false;
|
|
600
|
+
// Check event type matches
|
|
601
|
+
if (!pendingAwait.eventTypes.includes(eventType)) {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
// Check execution ID filter if specified
|
|
605
|
+
if (pendingAwait.executionIds && pendingAwait.executionIds.length > 0) {
|
|
606
|
+
if (!event.executionId ||
|
|
607
|
+
!pendingAwait.executionIds.includes(event.executionId)) {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Map workflow event type to awaitable event type.
|
|
615
|
+
*/
|
|
616
|
+
mapToAwaitableEventType(eventType) {
|
|
617
|
+
switch (eventType) {
|
|
618
|
+
case "step_completed":
|
|
619
|
+
return "step_completed";
|
|
620
|
+
case "step_failed":
|
|
621
|
+
return "step_failed";
|
|
622
|
+
case "user_response":
|
|
623
|
+
return "user_response";
|
|
624
|
+
case "escalation_resolved":
|
|
625
|
+
return "escalation_resolved";
|
|
626
|
+
default:
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Resolve an await condition (internal).
|
|
632
|
+
*
|
|
633
|
+
* Marks the await event as processed in the database for recovery tracking.
|
|
634
|
+
*/
|
|
635
|
+
resolveAwait(workflowId, resolvedBy) {
|
|
636
|
+
const pendingAwait = this.pendingAwaits.get(workflowId);
|
|
637
|
+
if (!pendingAwait)
|
|
638
|
+
return;
|
|
639
|
+
// Store for wakeup message context
|
|
640
|
+
this.resolvedAwaits.set(workflowId, { ...pendingAwait, resolvedBy });
|
|
641
|
+
// Clear from pending
|
|
642
|
+
this.pendingAwaits.delete(workflowId);
|
|
643
|
+
// Mark event as processed in database
|
|
644
|
+
const now = new Date().toISOString();
|
|
645
|
+
this.db
|
|
646
|
+
.prepare(`
|
|
647
|
+
UPDATE workflow_events
|
|
648
|
+
SET processed_at = ?,
|
|
649
|
+
payload = json_set(payload, '$.resolvedBy', ?)
|
|
650
|
+
WHERE id = ?
|
|
651
|
+
`)
|
|
652
|
+
.run(now, resolvedBy, pendingAwait.id);
|
|
653
|
+
// Clear timeout if exists
|
|
654
|
+
const timeoutId = this.awaitTimeouts.get(pendingAwait.id);
|
|
655
|
+
if (timeoutId) {
|
|
656
|
+
clearTimeout(timeoutId);
|
|
657
|
+
this.awaitTimeouts.delete(pendingAwait.id);
|
|
658
|
+
}
|
|
659
|
+
console.log(`[WakeupService] Resolved await ${pendingAwait.id} for workflow ${workflowId}`, { resolvedBy });
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Schedule timeout for an await.
|
|
663
|
+
*/
|
|
664
|
+
scheduleAwaitTimeout(workflowId, awaitId, seconds) {
|
|
665
|
+
// Clear any existing timeout for this await
|
|
666
|
+
const existingTimeout = this.awaitTimeouts.get(awaitId);
|
|
667
|
+
if (existingTimeout) {
|
|
668
|
+
clearTimeout(existingTimeout);
|
|
669
|
+
}
|
|
670
|
+
const timeout = setTimeout(async () => {
|
|
671
|
+
this.awaitTimeouts.delete(awaitId);
|
|
672
|
+
// Check if this await is still pending
|
|
673
|
+
const pendingAwait = this.pendingAwaits.get(workflowId);
|
|
674
|
+
if (!pendingAwait || pendingAwait.id !== awaitId) {
|
|
675
|
+
return; // Already resolved
|
|
676
|
+
}
|
|
677
|
+
// Resolve the await with "timeout" as the trigger
|
|
678
|
+
this.resolveAwait(workflowId, "timeout");
|
|
679
|
+
// Trigger wakeup immediately
|
|
680
|
+
try {
|
|
681
|
+
await this.triggerWakeup(workflowId);
|
|
682
|
+
}
|
|
683
|
+
catch (err) {
|
|
684
|
+
console.error(`[WakeupService] Failed to trigger timeout wakeup for workflow ${workflowId}:`, err);
|
|
685
|
+
}
|
|
686
|
+
}, seconds * 1000);
|
|
687
|
+
this.awaitTimeouts.set(awaitId, timeout);
|
|
688
|
+
console.log(`[WakeupService] Scheduled ${seconds}s timeout for await ${awaitId}`);
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Get the last resolved await for a workflow (for wakeup message).
|
|
692
|
+
* Clears after retrieval.
|
|
693
|
+
*/
|
|
694
|
+
getAndClearResolvedAwait(workflowId) {
|
|
695
|
+
const resolved = this.resolvedAwaits.get(workflowId);
|
|
696
|
+
if (resolved) {
|
|
697
|
+
this.resolvedAwaits.delete(workflowId);
|
|
698
|
+
}
|
|
699
|
+
return resolved;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Clear all await state for a workflow (on cancel/complete).
|
|
703
|
+
* Marks any pending await events as processed in the database.
|
|
704
|
+
*/
|
|
705
|
+
clearAwaitState(workflowId) {
|
|
706
|
+
const pendingAwait = this.pendingAwaits.get(workflowId);
|
|
707
|
+
if (pendingAwait) {
|
|
708
|
+
// Mark as processed (cancelled) in database
|
|
709
|
+
const now = new Date().toISOString();
|
|
710
|
+
this.db
|
|
711
|
+
.prepare(`
|
|
712
|
+
UPDATE workflow_events
|
|
713
|
+
SET processed_at = ?,
|
|
714
|
+
payload = json_set(payload, '$.resolvedBy', 'cancelled')
|
|
715
|
+
WHERE id = ?
|
|
716
|
+
`)
|
|
717
|
+
.run(now, pendingAwait.id);
|
|
718
|
+
const timeoutId = this.awaitTimeouts.get(pendingAwait.id);
|
|
719
|
+
if (timeoutId) {
|
|
720
|
+
clearTimeout(timeoutId);
|
|
721
|
+
this.awaitTimeouts.delete(pendingAwait.id);
|
|
722
|
+
}
|
|
723
|
+
this.pendingAwaits.delete(workflowId);
|
|
724
|
+
}
|
|
725
|
+
this.resolvedAwaits.delete(workflowId);
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Check if a workflow has a pending await condition.
|
|
729
|
+
*/
|
|
730
|
+
hasPendingAwait(workflowId) {
|
|
731
|
+
return this.pendingAwaits.has(workflowId);
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Get the pending await for a workflow (if any).
|
|
735
|
+
*/
|
|
736
|
+
getPendingAwait(workflowId) {
|
|
737
|
+
return this.pendingAwaits.get(workflowId);
|
|
738
|
+
}
|
|
739
|
+
// ===========================================================================
|
|
740
|
+
// Private Helpers
|
|
741
|
+
// ===========================================================================
|
|
742
|
+
/**
|
|
743
|
+
* Get a workflow by ID.
|
|
744
|
+
*/
|
|
745
|
+
getWorkflow(workflowId) {
|
|
746
|
+
const row = this.db
|
|
747
|
+
.prepare("SELECT * FROM workflows WHERE id = ?")
|
|
748
|
+
.get(workflowId);
|
|
749
|
+
if (!row) {
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
return {
|
|
753
|
+
id: row.id,
|
|
754
|
+
title: row.title,
|
|
755
|
+
source: JSON.parse(row.source),
|
|
756
|
+
status: row.status,
|
|
757
|
+
steps: JSON.parse(row.steps),
|
|
758
|
+
worktreePath: row.worktree_path ?? undefined,
|
|
759
|
+
branchName: row.branch_name ?? undefined,
|
|
760
|
+
baseBranch: row.base_branch,
|
|
761
|
+
currentStepIndex: row.current_step_index,
|
|
762
|
+
orchestratorExecutionId: row.orchestrator_execution_id ?? undefined,
|
|
763
|
+
orchestratorSessionId: row.orchestrator_session_id ?? undefined,
|
|
764
|
+
config: JSON.parse(row.config),
|
|
765
|
+
createdAt: row.created_at,
|
|
766
|
+
updatedAt: row.updated_at,
|
|
767
|
+
startedAt: row.started_at ?? undefined,
|
|
768
|
+
completedAt: row.completed_at ?? undefined,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Get executions referenced by events.
|
|
773
|
+
*/
|
|
774
|
+
getExecutionsForEvents(events) {
|
|
775
|
+
const executions = new Map();
|
|
776
|
+
// Collect unique execution IDs
|
|
777
|
+
const executionIds = new Set();
|
|
778
|
+
for (const event of events) {
|
|
779
|
+
if (event.executionId) {
|
|
780
|
+
executionIds.add(event.executionId);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
// Fetch executions
|
|
784
|
+
for (const executionId of executionIds) {
|
|
785
|
+
const row = this.db
|
|
786
|
+
.prepare("SELECT * FROM executions WHERE id = ?")
|
|
787
|
+
.get(executionId);
|
|
788
|
+
if (row) {
|
|
789
|
+
executions.set(executionId, row);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return executions;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Update the workflow's orchestrator execution ID.
|
|
796
|
+
*/
|
|
797
|
+
updateOrchestratorExecution(workflowId, executionId, sessionId) {
|
|
798
|
+
this.db
|
|
799
|
+
.prepare(`
|
|
800
|
+
UPDATE workflows
|
|
801
|
+
SET orchestrator_execution_id = ?,
|
|
802
|
+
orchestrator_session_id = ?,
|
|
803
|
+
updated_at = ?
|
|
804
|
+
WHERE id = ?
|
|
805
|
+
`)
|
|
806
|
+
.run(executionId, sessionId, new Date().toISOString(), workflowId);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
//# sourceMappingURL=wakeup-service.js.map
|