web-agent-bridge 3.0.0 → 3.3.0
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/LICENSE +72 -21
- package/README.ar.md +1286 -1073
- package/README.md +1764 -1535
- package/bin/agent-runner.js +474 -474
- package/bin/cli.js +237 -138
- package/bin/wab.js +80 -80
- package/examples/bidi-agent.js +119 -119
- package/examples/cross-site-agent.js +91 -91
- package/examples/mcp-agent.js +94 -94
- package/examples/next-app-router/README.md +44 -44
- package/examples/puppeteer-agent.js +108 -108
- package/examples/saas-dashboard/README.md +55 -55
- package/examples/shopify-hydrogen/README.md +74 -74
- package/examples/vision-agent.js +171 -171
- package/examples/wordpress-elementor/README.md +77 -77
- package/package.json +17 -3
- package/public/.well-known/agent-tools.json +180 -180
- package/public/.well-known/ai-assets.json +59 -59
- package/public/.well-known/ai-plugin.json +28 -0
- package/public/.well-known/security.txt +8 -0
- package/public/agent-workspace.html +349 -347
- package/public/ai.html +198 -196
- package/public/api.html +413 -0
- package/public/browser.html +486 -484
- package/public/commander-dashboard.html +243 -243
- package/public/cookies.html +210 -208
- package/public/css/agent-workspace.css +1713 -1713
- package/public/css/premium.css +317 -317
- package/public/css/styles.css +1235 -1235
- package/public/dashboard.html +706 -704
- package/public/demo.html +1770 -1
- package/public/dns.html +507 -0
- package/public/docs.html +587 -585
- package/public/feed.xml +89 -89
- package/public/growth.html +463 -0
- package/public/index.html +341 -9
- package/public/integrations.html +556 -0
- package/public/js/agent-workspace.js +1740 -1740
- package/public/js/auth-nav.js +31 -31
- package/public/js/auth-redirect.js +12 -12
- package/public/js/cookie-consent.js +56 -56
- package/public/js/wab-demo-page.js +721 -721
- package/public/js/ws-client.js +74 -74
- package/public/llms-full.txt +360 -309
- package/public/llms.txt +125 -86
- package/public/login.html +85 -83
- package/public/mesh-dashboard.html +328 -328
- package/public/openapi.json +580 -580
- package/public/phone-shield.html +281 -0
- package/public/premium-dashboard.html +2489 -2487
- package/public/premium.html +793 -791
- package/public/privacy.html +297 -295
- package/public/register.html +105 -103
- package/public/robots.txt +87 -87
- package/public/script/wab-consent.d.ts +36 -36
- package/public/script/wab-consent.js +104 -104
- package/public/script/wab-schema.js +131 -131
- package/public/script/wab.d.ts +108 -108
- package/public/script/wab.min.js +580 -580
- package/public/security.txt +8 -0
- package/public/terms.html +256 -254
- package/script/ai-agent-bridge.js +1754 -1754
- package/sdk/README.md +99 -99
- package/sdk/agent-mesh.js +449 -449
- package/sdk/commander.js +262 -262
- package/sdk/index.d.ts +464 -464
- package/sdk/index.js +18 -1
- package/sdk/multi-agent.js +318 -318
- package/sdk/package.json +12 -1
- package/sdk/safety-shield.js +219 -0
- package/sdk/schema-discovery.js +83 -83
- package/server/adapters/index.js +520 -520
- package/server/config/plans.js +367 -367
- package/server/config/secrets.js +102 -102
- package/server/control-plane/index.js +301 -301
- package/server/data-plane/index.js +354 -354
- package/server/index.js +175 -19
- package/server/llm/index.js +404 -404
- package/server/middleware/adminAuth.js +35 -35
- package/server/middleware/auth.js +50 -50
- package/server/middleware/featureGate.js +88 -88
- package/server/middleware/rateLimits.js +100 -100
- package/server/middleware/sensitiveAction.js +157 -0
- package/server/migrations/001_add_analytics_indexes.sql +7 -7
- package/server/migrations/002_premium_features.sql +418 -418
- package/server/migrations/003_ads_integer_cents.sql +33 -33
- package/server/migrations/004_agent_os.sql +158 -158
- package/server/migrations/005_marketplace_metering.sql +126 -126
- package/server/models/adapters/index.js +33 -33
- package/server/models/adapters/mysql.js +183 -183
- package/server/models/adapters/postgresql.js +172 -172
- package/server/models/adapters/sqlite.js +7 -7
- package/server/models/db.js +681 -681
- package/server/observability/failure-analysis.js +337 -337
- package/server/observability/index.js +394 -394
- package/server/protocol/capabilities.js +223 -223
- package/server/protocol/index.js +243 -243
- package/server/protocol/schema.js +584 -584
- package/server/registry/certification.js +271 -271
- package/server/registry/index.js +326 -326
- package/server/routes/admin-premium.js +671 -671
- package/server/routes/admin.js +261 -261
- package/server/routes/ads.js +130 -130
- package/server/routes/agent-workspace.js +540 -378
- package/server/routes/api.js +150 -150
- package/server/routes/auth.js +71 -71
- package/server/routes/billing.js +45 -45
- package/server/routes/commander.js +316 -316
- package/server/routes/demo-showcase.js +332 -0
- package/server/routes/demo-store.js +154 -0
- package/server/routes/discovery.js +417 -406
- package/server/routes/gateway.js +173 -0
- package/server/routes/license.js +251 -240
- package/server/routes/mesh.js +469 -469
- package/server/routes/noscript.js +543 -543
- package/server/routes/premium-v2.js +686 -686
- package/server/routes/premium.js +724 -724
- package/server/routes/runtime.js +2148 -2147
- package/server/routes/sovereign.js +465 -385
- package/server/routes/universal.js +200 -177
- package/server/routes/wab-api.js +850 -491
- package/server/runtime/container-worker.js +111 -111
- package/server/runtime/container.js +448 -448
- package/server/runtime/distributed-worker.js +362 -362
- package/server/runtime/event-bus.js +210 -210
- package/server/runtime/index.js +253 -253
- package/server/runtime/queue.js +599 -599
- package/server/runtime/replay.js +666 -666
- package/server/runtime/sandbox.js +266 -266
- package/server/runtime/scheduler.js +534 -534
- package/server/runtime/session-engine.js +293 -293
- package/server/runtime/state-manager.js +188 -188
- package/server/security/cross-site-redactor.js +196 -0
- package/server/security/dry-run.js +180 -0
- package/server/security/human-gate-rate-limit.js +147 -0
- package/server/security/human-gate-transports.js +178 -0
- package/server/security/human-gate.js +281 -0
- package/server/security/index.js +368 -368
- package/server/security/intent-engine.js +245 -0
- package/server/security/reward-guard.js +171 -0
- package/server/security/rollback-store.js +239 -0
- package/server/security/token-scope.js +404 -0
- package/server/security/url-policy.js +139 -0
- package/server/services/agent-chat.js +506 -506
- package/server/services/agent-learning.js +601 -575
- package/server/services/agent-memory.js +625 -625
- package/server/services/agent-mesh.js +555 -539
- package/server/services/agent-symphony.js +717 -717
- package/server/services/agent-tasks.js +1807 -1807
- package/server/services/api-key-engine.js +292 -0
- package/server/services/cluster.js +894 -894
- package/server/services/commander.js +738 -738
- package/server/services/edge-compute.js +440 -440
- package/server/services/email.js +204 -204
- package/server/services/hosted-runtime.js +205 -205
- package/server/services/lfd.js +635 -616
- package/server/services/local-ai.js +389 -389
- package/server/services/marketplace.js +270 -270
- package/server/services/metering.js +182 -182
- package/server/services/modules/affiliate-intelligence.js +93 -0
- package/server/services/modules/agent-firewall.js +90 -0
- package/server/services/modules/bounty.js +89 -0
- package/server/services/modules/collective-bargaining.js +92 -0
- package/server/services/modules/dark-pattern.js +66 -0
- package/server/services/modules/gov-intelligence.js +45 -0
- package/server/services/modules/neural.js +55 -0
- package/server/services/modules/notary.js +49 -0
- package/server/services/modules/price-time-machine.js +86 -0
- package/server/services/modules/protocol.js +104 -0
- package/server/services/negotiation.js +439 -439
- package/server/services/plugins.js +771 -771
- package/server/services/premium.js +1 -1
- package/server/services/price-intelligence.js +566 -565
- package/server/services/price-shield.js +1137 -1137
- package/server/services/reputation.js +465 -465
- package/server/services/search-engine.js +357 -357
- package/server/services/security.js +513 -513
- package/server/services/self-healing.js +843 -843
- package/server/services/sovereign-shield.js +542 -0
- package/server/services/stripe.js +192 -192
- package/server/services/swarm.js +788 -788
- package/server/services/universal-scraper.js +662 -661
- package/server/services/verification.js +481 -481
- package/server/services/vision.js +1163 -1163
- package/server/utils/cache.js +125 -125
- package/server/utils/migrate.js +81 -81
- package/server/utils/safe-fetch.js +228 -0
- package/server/utils/secureFields.js +50 -50
- package/server/ws.js +161 -161
- package/templates/artisan-marketplace.yaml +104 -104
- package/templates/book-price-scout.yaml +98 -98
- package/templates/electronics-price-tracker.yaml +108 -108
- package/templates/flight-deal-hunter.yaml +113 -113
- package/templates/freelancer-direct.yaml +116 -116
- package/templates/grocery-price-compare.yaml +93 -93
- package/templates/hotel-direct-booking.yaml +113 -113
- package/templates/local-services.yaml +98 -98
- package/templates/olive-oil-tunisia.yaml +88 -88
- package/templates/organic-farm-fresh.yaml +101 -101
- package/templates/restaurant-direct.yaml +97 -97
- package/server/services/fairness-engine.js +0 -409
- package/server/services/fairness.js +0 -420
|
@@ -1,534 +1,534 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* WAB Runtime - Task Scheduler
|
|
5
|
-
*
|
|
6
|
-
* Distributes and manages task execution with:
|
|
7
|
-
* - External queue backend (SQLite/Redis/Memory)
|
|
8
|
-
* - Priority queue with persistent storage
|
|
9
|
-
* - Retry logic with exponential backoff
|
|
10
|
-
* - Timeout management
|
|
11
|
-
* - Dependency resolution
|
|
12
|
-
* - Concurrency control
|
|
13
|
-
* - Deterministic replay integration
|
|
14
|
-
* - Container isolation support
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const crypto = require('crypto');
|
|
18
|
-
const { bus } = require('./event-bus');
|
|
19
|
-
const { createQueue } = require('./queue');
|
|
20
|
-
|
|
21
|
-
// Task states
|
|
22
|
-
const TaskState = {
|
|
23
|
-
QUEUED: 'queued',
|
|
24
|
-
SCHEDULED: 'scheduled',
|
|
25
|
-
RUNNING: 'running',
|
|
26
|
-
PAUSED: 'paused',
|
|
27
|
-
COMPLETED: 'completed',
|
|
28
|
-
FAILED: 'failed',
|
|
29
|
-
CANCELLED: 'cancelled',
|
|
30
|
-
RETRYING: 'retrying',
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
class Scheduler {
|
|
34
|
-
constructor(options = {}) {
|
|
35
|
-
this._queue = []; // fallback in-memory queue
|
|
36
|
-
this._externalQueue = null; // external queue backend
|
|
37
|
-
this._running = new Map(); // taskId → task
|
|
38
|
-
this._completed = new Map(); // taskId → result (limited buffer)
|
|
39
|
-
this._handlers = new Map(); // task type → handler function
|
|
40
|
-
this._replayEngine = null; // optional replay integration
|
|
41
|
-
this._containerRunner = null; // optional container isolation
|
|
42
|
-
this._maxConcurrent = options.maxConcurrent || 20;
|
|
43
|
-
this._maxQueueSize = options.maxQueueSize || 1000;
|
|
44
|
-
this._maxCompleted = options.maxCompleted || 500;
|
|
45
|
-
this._defaultRetries = options.defaultRetries || 3;
|
|
46
|
-
this._defaultTimeout = options.defaultTimeout || 30000;
|
|
47
|
-
this._processing = false;
|
|
48
|
-
this._stats = { submitted: 0, completed: 0, failed: 0, retried: 0, cancelled: 0, timedOut: 0 };
|
|
49
|
-
|
|
50
|
-
// Initialize external queue
|
|
51
|
-
try {
|
|
52
|
-
this._externalQueue = createQueue('scheduler', {
|
|
53
|
-
maxRetries: this._defaultRetries,
|
|
54
|
-
processTimeout: this._defaultTimeout,
|
|
55
|
-
});
|
|
56
|
-
} catch {
|
|
57
|
-
// Fallback to in-memory
|
|
58
|
-
this._externalQueue = null;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Attach the replay engine for deterministic recording
|
|
64
|
-
*/
|
|
65
|
-
setReplayEngine(engine) {
|
|
66
|
-
this._replayEngine = engine;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Attach the container runner for process isolation
|
|
71
|
-
*/
|
|
72
|
-
setContainerRunner(runner) {
|
|
73
|
-
this._containerRunner = runner;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Register a handler for a task type
|
|
78
|
-
*/
|
|
79
|
-
registerHandler(taskType, handler) {
|
|
80
|
-
this._handlers.set(taskType, handler);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Submit a task to the scheduler
|
|
85
|
-
*/
|
|
86
|
-
submit(task) {
|
|
87
|
-
const taskEntry = {
|
|
88
|
-
id: task.id || `task_${crypto.randomBytes(16).toString('hex')}`,
|
|
89
|
-
type: task.type || 'general',
|
|
90
|
-
objective: task.objective || '',
|
|
91
|
-
params: task.params || {},
|
|
92
|
-
steps: task.steps || [],
|
|
93
|
-
priority: task.priority || 50,
|
|
94
|
-
agentId: task.agentId || null,
|
|
95
|
-
siteId: task.siteId || null,
|
|
96
|
-
traceId: task.traceId || `trace_${crypto.randomBytes(16).toString('hex')}`,
|
|
97
|
-
|
|
98
|
-
// Execution config
|
|
99
|
-
retries: task.retries !== undefined ? task.retries : this._defaultRetries,
|
|
100
|
-
retriesLeft: task.retries !== undefined ? task.retries : this._defaultRetries,
|
|
101
|
-
timeout: task.timeout || this._defaultTimeout,
|
|
102
|
-
deadline: task.deadline || null,
|
|
103
|
-
isolate: task.isolate || false, // run in container if true
|
|
104
|
-
|
|
105
|
-
// Dependencies
|
|
106
|
-
dependsOn: task.dependsOn || [],
|
|
107
|
-
|
|
108
|
-
// State
|
|
109
|
-
state: TaskState.QUEUED,
|
|
110
|
-
attempt: 0,
|
|
111
|
-
currentStep: 0,
|
|
112
|
-
result: null,
|
|
113
|
-
error: null,
|
|
114
|
-
progress: 0,
|
|
115
|
-
|
|
116
|
-
// Timestamps
|
|
117
|
-
submittedAt: Date.now(),
|
|
118
|
-
scheduledAt: null,
|
|
119
|
-
startedAt: null,
|
|
120
|
-
completedAt: null,
|
|
121
|
-
|
|
122
|
-
// Checkpoints (for resume/rollback)
|
|
123
|
-
checkpoints: [],
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
// Add to queue (external or in-memory)
|
|
127
|
-
if (this._externalQueue) {
|
|
128
|
-
this._externalQueue.enqueue({
|
|
129
|
-
type: taskEntry.type,
|
|
130
|
-
data: taskEntry,
|
|
131
|
-
priority: taskEntry.priority,
|
|
132
|
-
maxAttempts: taskEntry.retries + 1,
|
|
133
|
-
timeoutMs: taskEntry.timeout,
|
|
134
|
-
groupId: taskEntry.agentId || null,
|
|
135
|
-
});
|
|
136
|
-
} else {
|
|
137
|
-
if (this._queue.length >= this._maxQueueSize) {
|
|
138
|
-
throw new Error('Task queue is full');
|
|
139
|
-
}
|
|
140
|
-
// Insert by priority (higher priority = earlier in queue)
|
|
141
|
-
let inserted = false;
|
|
142
|
-
for (let i = 0; i < this._queue.length; i++) {
|
|
143
|
-
if (taskEntry.priority > this._queue[i].priority) {
|
|
144
|
-
this._queue.splice(i, 0, taskEntry);
|
|
145
|
-
inserted = true;
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
if (!inserted) this._queue.push(taskEntry);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
this._stats.submitted++;
|
|
153
|
-
bus.emit('task.queued', { taskId: taskEntry.id, type: taskEntry.type, priority: taskEntry.priority });
|
|
154
|
-
|
|
155
|
-
// Try to process immediately
|
|
156
|
-
this._processQueue();
|
|
157
|
-
|
|
158
|
-
return {
|
|
159
|
-
taskId: taskEntry.id,
|
|
160
|
-
status: taskEntry.state,
|
|
161
|
-
position: this._queue.indexOf(taskEntry),
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Process the task queue
|
|
167
|
-
*/
|
|
168
|
-
async _processQueue() {
|
|
169
|
-
if (this._processing) return;
|
|
170
|
-
this._processing = true;
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
while (this._running.size < this._maxConcurrent) {
|
|
174
|
-
let task = null;
|
|
175
|
-
|
|
176
|
-
if (this._externalQueue) {
|
|
177
|
-
// Dequeue from external queue
|
|
178
|
-
const item = this._externalQueue.dequeue();
|
|
179
|
-
if (!item) break;
|
|
180
|
-
task = item.data;
|
|
181
|
-
task._queueItemId = item.id;
|
|
182
|
-
} else {
|
|
183
|
-
// Dequeue from in-memory queue
|
|
184
|
-
if (this._queue.length === 0) break;
|
|
185
|
-
task = this._findNextReady();
|
|
186
|
-
if (!task) break;
|
|
187
|
-
const idx = this._queue.indexOf(task);
|
|
188
|
-
if (idx !== -1) this._queue.splice(idx, 1);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Check deadline
|
|
192
|
-
if (task.deadline && Date.now() > task.deadline) {
|
|
193
|
-
task.state = TaskState.CANCELLED;
|
|
194
|
-
task.error = { code: 'DEADLINE_EXCEEDED', message: 'Task deadline has passed' };
|
|
195
|
-
if (this._externalQueue && task._queueItemId) {
|
|
196
|
-
this._externalQueue.fail(task._queueItemId, 'Deadline exceeded');
|
|
197
|
-
}
|
|
198
|
-
bus.emit('task.cancelled', { taskId: task.id, reason: 'deadline' });
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Execute
|
|
203
|
-
this._running.set(task.id, task);
|
|
204
|
-
this._executeTask(task); // fire and forget
|
|
205
|
-
}
|
|
206
|
-
} finally {
|
|
207
|
-
this._processing = false;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Find next task whose dependencies are satisfied
|
|
213
|
-
*/
|
|
214
|
-
_findNextReady() {
|
|
215
|
-
for (const task of this._queue) {
|
|
216
|
-
if (task.dependsOn.length === 0) return task;
|
|
217
|
-
const allDone = task.dependsOn.every(depId => {
|
|
218
|
-
const dep = this._completed.get(depId);
|
|
219
|
-
return dep && dep.state === TaskState.COMPLETED;
|
|
220
|
-
});
|
|
221
|
-
if (allDone) return task;
|
|
222
|
-
}
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Execute a single task
|
|
228
|
-
*/
|
|
229
|
-
async _executeTask(task) {
|
|
230
|
-
task.state = TaskState.RUNNING;
|
|
231
|
-
task.startedAt = Date.now();
|
|
232
|
-
task.attempt++;
|
|
233
|
-
|
|
234
|
-
bus.emit('task.started', { taskId: task.id, attempt: task.attempt, type: task.type });
|
|
235
|
-
|
|
236
|
-
// Start deterministic recording
|
|
237
|
-
if (this._replayEngine) {
|
|
238
|
-
this._replayEngine.startRecording(task.id, {
|
|
239
|
-
type: task.type,
|
|
240
|
-
params: task.params,
|
|
241
|
-
objective: task.objective,
|
|
242
|
-
attempt: task.attempt,
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const handler = this._handlers.get(task.type);
|
|
247
|
-
if (!handler) {
|
|
248
|
-
task.state = TaskState.FAILED;
|
|
249
|
-
task.error = { code: 'NO_HANDLER', message: `No handler for task type: ${task.type}` };
|
|
250
|
-
if (this._replayEngine) {
|
|
251
|
-
this._replayEngine.completeRecording(task.id, null, task.error);
|
|
252
|
-
}
|
|
253
|
-
this._finishTask(task);
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
try {
|
|
258
|
-
let result;
|
|
259
|
-
|
|
260
|
-
const ctx = {
|
|
261
|
-
taskId: task.id,
|
|
262
|
-
agentId: task.agentId,
|
|
263
|
-
siteId: task.siteId,
|
|
264
|
-
traceId: task.traceId,
|
|
265
|
-
steps: task.steps,
|
|
266
|
-
attempt: task.attempt,
|
|
267
|
-
reportProgress: (pct, step) => {
|
|
268
|
-
task.progress = pct;
|
|
269
|
-
if (step !== undefined) task.currentStep = step;
|
|
270
|
-
bus.emit('task.progress', { taskId: task.id, progress: pct, step });
|
|
271
|
-
},
|
|
272
|
-
checkpoint: (data) => {
|
|
273
|
-
task.checkpoints.push({ data, timestamp: Date.now(), step: task.currentStep });
|
|
274
|
-
if (this._replayEngine) {
|
|
275
|
-
this._replayEngine.saveCheckpoint(task.id, `step-${task.currentStep}`, data);
|
|
276
|
-
}
|
|
277
|
-
},
|
|
278
|
-
recordStep: (step) => {
|
|
279
|
-
if (this._replayEngine) {
|
|
280
|
-
this._replayEngine.recordStep(task.id, step);
|
|
281
|
-
}
|
|
282
|
-
},
|
|
283
|
-
recordSideEffect: (effect) => {
|
|
284
|
-
if (this._replayEngine) {
|
|
285
|
-
this._replayEngine.recordSideEffect(task.id, effect);
|
|
286
|
-
}
|
|
287
|
-
},
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
// Execute in container if isolation requested
|
|
291
|
-
if (task.isolate && this._containerRunner && typeof task.params === 'object' && task.params._code) {
|
|
292
|
-
const containerResult = await this._containerRunner.runInProcess(task.id, task.params._code, {
|
|
293
|
-
params: task.params,
|
|
294
|
-
timeout: task.timeout,
|
|
295
|
-
maxMemory: task.params._maxMemory || 256 * 1024 * 1024,
|
|
296
|
-
});
|
|
297
|
-
if (!containerResult.success) {
|
|
298
|
-
throw new Error(containerResult.error || 'Container execution failed');
|
|
299
|
-
}
|
|
300
|
-
result = containerResult.result;
|
|
301
|
-
} else {
|
|
302
|
-
result = await _withTimeout(handler(task.params, ctx), task.timeout);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
task.state = TaskState.COMPLETED;
|
|
306
|
-
task.result = result;
|
|
307
|
-
task.progress = 100;
|
|
308
|
-
this._stats.completed++;
|
|
309
|
-
|
|
310
|
-
// Complete recording
|
|
311
|
-
if (this._replayEngine) {
|
|
312
|
-
this._replayEngine.completeRecording(task.id, result);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Mark complete in external queue
|
|
316
|
-
if (this._externalQueue && task._queueItemId) {
|
|
317
|
-
this._externalQueue.complete(task._queueItemId, result);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
bus.emit('task.completed', { taskId: task.id, duration: Date.now() - task.startedAt });
|
|
321
|
-
} catch (err) {
|
|
322
|
-
const isTimeout = err.message.includes('timed out');
|
|
323
|
-
if (isTimeout) this._stats.timedOut++;
|
|
324
|
-
|
|
325
|
-
if (task.retriesLeft > 0 && !isTimeout) {
|
|
326
|
-
// Retry with exponential backoff
|
|
327
|
-
task.retriesLeft--;
|
|
328
|
-
task.state = TaskState.RETRYING;
|
|
329
|
-
this._stats.retried++;
|
|
330
|
-
const backoff = Math.min(1000 * Math.pow(2, task.attempt - 1), 30000);
|
|
331
|
-
bus.emit('task.retrying', { taskId: task.id, attempt: task.attempt, backoff });
|
|
332
|
-
|
|
333
|
-
setTimeout(() => {
|
|
334
|
-
this._running.delete(task.id);
|
|
335
|
-
if (this._externalQueue) {
|
|
336
|
-
// Re-enqueue in external queue
|
|
337
|
-
this._externalQueue.enqueue({
|
|
338
|
-
type: task.type,
|
|
339
|
-
data: task,
|
|
340
|
-
priority: task.priority,
|
|
341
|
-
maxAttempts: task.retriesLeft + 1,
|
|
342
|
-
timeoutMs: task.timeout,
|
|
343
|
-
});
|
|
344
|
-
} else {
|
|
345
|
-
this._queue.unshift(task);
|
|
346
|
-
}
|
|
347
|
-
this._processQueue();
|
|
348
|
-
}, backoff);
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
task.state = TaskState.FAILED;
|
|
353
|
-
task.error = { code: isTimeout ? 'TIMEOUT' : 'EXECUTION_ERROR', message: err.message };
|
|
354
|
-
this._stats.failed++;
|
|
355
|
-
|
|
356
|
-
// Complete recording with error
|
|
357
|
-
if (this._replayEngine) {
|
|
358
|
-
this._replayEngine.completeRecording(task.id, null, err);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Mark failed in external queue
|
|
362
|
-
if (this._externalQueue && task._queueItemId) {
|
|
363
|
-
this._externalQueue.fail(task._queueItemId, err.message);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
bus.emit('task.failed', { taskId: task.id, error: err.message, attempts: task.attempt });
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
this._finishTask(task);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Finish a task (move to completed buffer)
|
|
374
|
-
*/
|
|
375
|
-
_finishTask(task) {
|
|
376
|
-
task.completedAt = Date.now();
|
|
377
|
-
this._running.delete(task.id);
|
|
378
|
-
|
|
379
|
-
// Store in completed buffer
|
|
380
|
-
this._completed.set(task.id, task);
|
|
381
|
-
if (this._completed.size > this._maxCompleted) {
|
|
382
|
-
const oldest = this._completed.keys().next().value;
|
|
383
|
-
this._completed.delete(oldest);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Process more tasks
|
|
387
|
-
this._processQueue();
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Get task status
|
|
392
|
-
*/
|
|
393
|
-
getTask(taskId) {
|
|
394
|
-
// Check running
|
|
395
|
-
let task = this._running.get(taskId);
|
|
396
|
-
if (!task) {
|
|
397
|
-
// Check queue
|
|
398
|
-
task = this._queue.find(t => t.id === taskId);
|
|
399
|
-
}
|
|
400
|
-
if (!task) {
|
|
401
|
-
// Check completed
|
|
402
|
-
task = this._completed.get(taskId);
|
|
403
|
-
}
|
|
404
|
-
if (!task) return null;
|
|
405
|
-
|
|
406
|
-
return {
|
|
407
|
-
id: task.id,
|
|
408
|
-
type: task.type,
|
|
409
|
-
objective: task.objective,
|
|
410
|
-
state: task.state,
|
|
411
|
-
progress: task.progress,
|
|
412
|
-
currentStep: task.currentStep,
|
|
413
|
-
totalSteps: task.steps.length,
|
|
414
|
-
attempt: task.attempt,
|
|
415
|
-
result: task.result,
|
|
416
|
-
error: task.error,
|
|
417
|
-
checkpoints: task.checkpoints.length,
|
|
418
|
-
submittedAt: task.submittedAt,
|
|
419
|
-
startedAt: task.startedAt,
|
|
420
|
-
completedAt: task.completedAt,
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Cancel a task
|
|
426
|
-
*/
|
|
427
|
-
cancel(taskId) {
|
|
428
|
-
// Remove from queue
|
|
429
|
-
const idx = this._queue.findIndex(t => t.id === taskId);
|
|
430
|
-
if (idx !== -1) {
|
|
431
|
-
const task = this._queue.splice(idx, 1)[0];
|
|
432
|
-
task.state = TaskState.CANCELLED;
|
|
433
|
-
task.completedAt = Date.now();
|
|
434
|
-
this._completed.set(task.id, task);
|
|
435
|
-
this._stats.cancelled++;
|
|
436
|
-
bus.emit('task.cancelled', { taskId, reason: 'user' });
|
|
437
|
-
return true;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Mark running task as cancelled (handler must check)
|
|
441
|
-
const running = this._running.get(taskId);
|
|
442
|
-
if (running) {
|
|
443
|
-
running.state = TaskState.CANCELLED;
|
|
444
|
-
this._stats.cancelled++;
|
|
445
|
-
bus.emit('task.cancelled', { taskId, reason: 'user' });
|
|
446
|
-
return true;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
return false;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Pause a running task (handler must support this)
|
|
454
|
-
*/
|
|
455
|
-
pause(taskId) {
|
|
456
|
-
const task = this._running.get(taskId);
|
|
457
|
-
if (task && task.state === TaskState.RUNNING) {
|
|
458
|
-
task.state = TaskState.PAUSED;
|
|
459
|
-
bus.emit('task.paused', { taskId });
|
|
460
|
-
return true;
|
|
461
|
-
}
|
|
462
|
-
return false;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Resume a paused task
|
|
467
|
-
*/
|
|
468
|
-
resume(taskId) {
|
|
469
|
-
const task = this._running.get(taskId);
|
|
470
|
-
if (task && task.state === TaskState.PAUSED) {
|
|
471
|
-
task.state = TaskState.RUNNING;
|
|
472
|
-
bus.emit('task.resumed', { taskId });
|
|
473
|
-
return true;
|
|
474
|
-
}
|
|
475
|
-
return false;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/**
|
|
479
|
-
* Get scheduler stats
|
|
480
|
-
*/
|
|
481
|
-
getStats() {
|
|
482
|
-
const queueSize = this._externalQueue
|
|
483
|
-
? this._externalQueue.size()
|
|
484
|
-
: this._queue.length;
|
|
485
|
-
|
|
486
|
-
return {
|
|
487
|
-
...this._stats,
|
|
488
|
-
queueSize,
|
|
489
|
-
queueBackend: this._externalQueue ? this._externalQueue.constructor.name : 'memory',
|
|
490
|
-
runningCount: this._running.size,
|
|
491
|
-
completedBufferSize: this._completed.size,
|
|
492
|
-
replayEnabled: !!this._replayEngine,
|
|
493
|
-
containerEnabled: !!this._containerRunner,
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* List tasks by state
|
|
499
|
-
*/
|
|
500
|
-
listTasks(state, limit = 50) {
|
|
501
|
-
const tasks = [];
|
|
502
|
-
|
|
503
|
-
if (!state || state === TaskState.QUEUED) {
|
|
504
|
-
for (const t of this._queue.slice(0, limit)) {
|
|
505
|
-
tasks.push(this.getTask(t.id));
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
if (!state || state === TaskState.RUNNING) {
|
|
509
|
-
for (const [, t] of this._running) {
|
|
510
|
-
if (tasks.length >= limit) break;
|
|
511
|
-
tasks.push(this.getTask(t.id));
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
if (!state || [TaskState.COMPLETED, TaskState.FAILED, TaskState.CANCELLED].includes(state)) {
|
|
515
|
-
for (const [, t] of this._completed) {
|
|
516
|
-
if (tasks.length >= limit) break;
|
|
517
|
-
if (!state || t.state === state) tasks.push(this.getTask(t.id));
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
return tasks.slice(0, limit);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
function _withTimeout(promise, ms) {
|
|
526
|
-
if (!ms || ms <= 0) return promise;
|
|
527
|
-
return new Promise((resolve, reject) => {
|
|
528
|
-
const timer = setTimeout(() => reject(new Error(`Task timed out after ${ms}ms`)), ms);
|
|
529
|
-
promise.then(r => { clearTimeout(timer); resolve(r); })
|
|
530
|
-
.catch(e => { clearTimeout(timer); reject(e); });
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
module.exports = { Scheduler, TaskState };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WAB Runtime - Task Scheduler
|
|
5
|
+
*
|
|
6
|
+
* Distributes and manages task execution with:
|
|
7
|
+
* - External queue backend (SQLite/Redis/Memory)
|
|
8
|
+
* - Priority queue with persistent storage
|
|
9
|
+
* - Retry logic with exponential backoff
|
|
10
|
+
* - Timeout management
|
|
11
|
+
* - Dependency resolution
|
|
12
|
+
* - Concurrency control
|
|
13
|
+
* - Deterministic replay integration
|
|
14
|
+
* - Container isolation support
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
const { bus } = require('./event-bus');
|
|
19
|
+
const { createQueue } = require('./queue');
|
|
20
|
+
|
|
21
|
+
// Task states
|
|
22
|
+
const TaskState = {
|
|
23
|
+
QUEUED: 'queued',
|
|
24
|
+
SCHEDULED: 'scheduled',
|
|
25
|
+
RUNNING: 'running',
|
|
26
|
+
PAUSED: 'paused',
|
|
27
|
+
COMPLETED: 'completed',
|
|
28
|
+
FAILED: 'failed',
|
|
29
|
+
CANCELLED: 'cancelled',
|
|
30
|
+
RETRYING: 'retrying',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
class Scheduler {
|
|
34
|
+
constructor(options = {}) {
|
|
35
|
+
this._queue = []; // fallback in-memory queue
|
|
36
|
+
this._externalQueue = null; // external queue backend
|
|
37
|
+
this._running = new Map(); // taskId → task
|
|
38
|
+
this._completed = new Map(); // taskId → result (limited buffer)
|
|
39
|
+
this._handlers = new Map(); // task type → handler function
|
|
40
|
+
this._replayEngine = null; // optional replay integration
|
|
41
|
+
this._containerRunner = null; // optional container isolation
|
|
42
|
+
this._maxConcurrent = options.maxConcurrent || 20;
|
|
43
|
+
this._maxQueueSize = options.maxQueueSize || 1000;
|
|
44
|
+
this._maxCompleted = options.maxCompleted || 500;
|
|
45
|
+
this._defaultRetries = options.defaultRetries || 3;
|
|
46
|
+
this._defaultTimeout = options.defaultTimeout || 30000;
|
|
47
|
+
this._processing = false;
|
|
48
|
+
this._stats = { submitted: 0, completed: 0, failed: 0, retried: 0, cancelled: 0, timedOut: 0 };
|
|
49
|
+
|
|
50
|
+
// Initialize external queue
|
|
51
|
+
try {
|
|
52
|
+
this._externalQueue = createQueue('scheduler', {
|
|
53
|
+
maxRetries: this._defaultRetries,
|
|
54
|
+
processTimeout: this._defaultTimeout,
|
|
55
|
+
});
|
|
56
|
+
} catch {
|
|
57
|
+
// Fallback to in-memory
|
|
58
|
+
this._externalQueue = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Attach the replay engine for deterministic recording
|
|
64
|
+
*/
|
|
65
|
+
setReplayEngine(engine) {
|
|
66
|
+
this._replayEngine = engine;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Attach the container runner for process isolation
|
|
71
|
+
*/
|
|
72
|
+
setContainerRunner(runner) {
|
|
73
|
+
this._containerRunner = runner;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Register a handler for a task type
|
|
78
|
+
*/
|
|
79
|
+
registerHandler(taskType, handler) {
|
|
80
|
+
this._handlers.set(taskType, handler);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Submit a task to the scheduler
|
|
85
|
+
*/
|
|
86
|
+
submit(task) {
|
|
87
|
+
const taskEntry = {
|
|
88
|
+
id: task.id || `task_${crypto.randomBytes(16).toString('hex')}`,
|
|
89
|
+
type: task.type || 'general',
|
|
90
|
+
objective: task.objective || '',
|
|
91
|
+
params: task.params || {},
|
|
92
|
+
steps: task.steps || [],
|
|
93
|
+
priority: task.priority || 50,
|
|
94
|
+
agentId: task.agentId || null,
|
|
95
|
+
siteId: task.siteId || null,
|
|
96
|
+
traceId: task.traceId || `trace_${crypto.randomBytes(16).toString('hex')}`,
|
|
97
|
+
|
|
98
|
+
// Execution config
|
|
99
|
+
retries: task.retries !== undefined ? task.retries : this._defaultRetries,
|
|
100
|
+
retriesLeft: task.retries !== undefined ? task.retries : this._defaultRetries,
|
|
101
|
+
timeout: task.timeout || this._defaultTimeout,
|
|
102
|
+
deadline: task.deadline || null,
|
|
103
|
+
isolate: task.isolate || false, // run in container if true
|
|
104
|
+
|
|
105
|
+
// Dependencies
|
|
106
|
+
dependsOn: task.dependsOn || [],
|
|
107
|
+
|
|
108
|
+
// State
|
|
109
|
+
state: TaskState.QUEUED,
|
|
110
|
+
attempt: 0,
|
|
111
|
+
currentStep: 0,
|
|
112
|
+
result: null,
|
|
113
|
+
error: null,
|
|
114
|
+
progress: 0,
|
|
115
|
+
|
|
116
|
+
// Timestamps
|
|
117
|
+
submittedAt: Date.now(),
|
|
118
|
+
scheduledAt: null,
|
|
119
|
+
startedAt: null,
|
|
120
|
+
completedAt: null,
|
|
121
|
+
|
|
122
|
+
// Checkpoints (for resume/rollback)
|
|
123
|
+
checkpoints: [],
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Add to queue (external or in-memory)
|
|
127
|
+
if (this._externalQueue) {
|
|
128
|
+
this._externalQueue.enqueue({
|
|
129
|
+
type: taskEntry.type,
|
|
130
|
+
data: taskEntry,
|
|
131
|
+
priority: taskEntry.priority,
|
|
132
|
+
maxAttempts: taskEntry.retries + 1,
|
|
133
|
+
timeoutMs: taskEntry.timeout,
|
|
134
|
+
groupId: taskEntry.agentId || null,
|
|
135
|
+
});
|
|
136
|
+
} else {
|
|
137
|
+
if (this._queue.length >= this._maxQueueSize) {
|
|
138
|
+
throw new Error('Task queue is full');
|
|
139
|
+
}
|
|
140
|
+
// Insert by priority (higher priority = earlier in queue)
|
|
141
|
+
let inserted = false;
|
|
142
|
+
for (let i = 0; i < this._queue.length; i++) {
|
|
143
|
+
if (taskEntry.priority > this._queue[i].priority) {
|
|
144
|
+
this._queue.splice(i, 0, taskEntry);
|
|
145
|
+
inserted = true;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!inserted) this._queue.push(taskEntry);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this._stats.submitted++;
|
|
153
|
+
bus.emit('task.queued', { taskId: taskEntry.id, type: taskEntry.type, priority: taskEntry.priority });
|
|
154
|
+
|
|
155
|
+
// Try to process immediately
|
|
156
|
+
this._processQueue();
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
taskId: taskEntry.id,
|
|
160
|
+
status: taskEntry.state,
|
|
161
|
+
position: this._queue.indexOf(taskEntry),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Process the task queue
|
|
167
|
+
*/
|
|
168
|
+
async _processQueue() {
|
|
169
|
+
if (this._processing) return;
|
|
170
|
+
this._processing = true;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
while (this._running.size < this._maxConcurrent) {
|
|
174
|
+
let task = null;
|
|
175
|
+
|
|
176
|
+
if (this._externalQueue) {
|
|
177
|
+
// Dequeue from external queue
|
|
178
|
+
const item = this._externalQueue.dequeue();
|
|
179
|
+
if (!item) break;
|
|
180
|
+
task = item.data;
|
|
181
|
+
task._queueItemId = item.id;
|
|
182
|
+
} else {
|
|
183
|
+
// Dequeue from in-memory queue
|
|
184
|
+
if (this._queue.length === 0) break;
|
|
185
|
+
task = this._findNextReady();
|
|
186
|
+
if (!task) break;
|
|
187
|
+
const idx = this._queue.indexOf(task);
|
|
188
|
+
if (idx !== -1) this._queue.splice(idx, 1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check deadline
|
|
192
|
+
if (task.deadline && Date.now() > task.deadline) {
|
|
193
|
+
task.state = TaskState.CANCELLED;
|
|
194
|
+
task.error = { code: 'DEADLINE_EXCEEDED', message: 'Task deadline has passed' };
|
|
195
|
+
if (this._externalQueue && task._queueItemId) {
|
|
196
|
+
this._externalQueue.fail(task._queueItemId, 'Deadline exceeded');
|
|
197
|
+
}
|
|
198
|
+
bus.emit('task.cancelled', { taskId: task.id, reason: 'deadline' });
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Execute
|
|
203
|
+
this._running.set(task.id, task);
|
|
204
|
+
this._executeTask(task); // fire and forget
|
|
205
|
+
}
|
|
206
|
+
} finally {
|
|
207
|
+
this._processing = false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Find next task whose dependencies are satisfied
|
|
213
|
+
*/
|
|
214
|
+
_findNextReady() {
|
|
215
|
+
for (const task of this._queue) {
|
|
216
|
+
if (task.dependsOn.length === 0) return task;
|
|
217
|
+
const allDone = task.dependsOn.every(depId => {
|
|
218
|
+
const dep = this._completed.get(depId);
|
|
219
|
+
return dep && dep.state === TaskState.COMPLETED;
|
|
220
|
+
});
|
|
221
|
+
if (allDone) return task;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Execute a single task
|
|
228
|
+
*/
|
|
229
|
+
async _executeTask(task) {
|
|
230
|
+
task.state = TaskState.RUNNING;
|
|
231
|
+
task.startedAt = Date.now();
|
|
232
|
+
task.attempt++;
|
|
233
|
+
|
|
234
|
+
bus.emit('task.started', { taskId: task.id, attempt: task.attempt, type: task.type });
|
|
235
|
+
|
|
236
|
+
// Start deterministic recording
|
|
237
|
+
if (this._replayEngine) {
|
|
238
|
+
this._replayEngine.startRecording(task.id, {
|
|
239
|
+
type: task.type,
|
|
240
|
+
params: task.params,
|
|
241
|
+
objective: task.objective,
|
|
242
|
+
attempt: task.attempt,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const handler = this._handlers.get(task.type);
|
|
247
|
+
if (!handler) {
|
|
248
|
+
task.state = TaskState.FAILED;
|
|
249
|
+
task.error = { code: 'NO_HANDLER', message: `No handler for task type: ${task.type}` };
|
|
250
|
+
if (this._replayEngine) {
|
|
251
|
+
this._replayEngine.completeRecording(task.id, null, task.error);
|
|
252
|
+
}
|
|
253
|
+
this._finishTask(task);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
let result;
|
|
259
|
+
|
|
260
|
+
const ctx = {
|
|
261
|
+
taskId: task.id,
|
|
262
|
+
agentId: task.agentId,
|
|
263
|
+
siteId: task.siteId,
|
|
264
|
+
traceId: task.traceId,
|
|
265
|
+
steps: task.steps,
|
|
266
|
+
attempt: task.attempt,
|
|
267
|
+
reportProgress: (pct, step) => {
|
|
268
|
+
task.progress = pct;
|
|
269
|
+
if (step !== undefined) task.currentStep = step;
|
|
270
|
+
bus.emit('task.progress', { taskId: task.id, progress: pct, step });
|
|
271
|
+
},
|
|
272
|
+
checkpoint: (data) => {
|
|
273
|
+
task.checkpoints.push({ data, timestamp: Date.now(), step: task.currentStep });
|
|
274
|
+
if (this._replayEngine) {
|
|
275
|
+
this._replayEngine.saveCheckpoint(task.id, `step-${task.currentStep}`, data);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
recordStep: (step) => {
|
|
279
|
+
if (this._replayEngine) {
|
|
280
|
+
this._replayEngine.recordStep(task.id, step);
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
recordSideEffect: (effect) => {
|
|
284
|
+
if (this._replayEngine) {
|
|
285
|
+
this._replayEngine.recordSideEffect(task.id, effect);
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// Execute in container if isolation requested
|
|
291
|
+
if (task.isolate && this._containerRunner && typeof task.params === 'object' && task.params._code) {
|
|
292
|
+
const containerResult = await this._containerRunner.runInProcess(task.id, task.params._code, {
|
|
293
|
+
params: task.params,
|
|
294
|
+
timeout: task.timeout,
|
|
295
|
+
maxMemory: task.params._maxMemory || 256 * 1024 * 1024,
|
|
296
|
+
});
|
|
297
|
+
if (!containerResult.success) {
|
|
298
|
+
throw new Error(containerResult.error || 'Container execution failed');
|
|
299
|
+
}
|
|
300
|
+
result = containerResult.result;
|
|
301
|
+
} else {
|
|
302
|
+
result = await _withTimeout(handler(task.params, ctx), task.timeout);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
task.state = TaskState.COMPLETED;
|
|
306
|
+
task.result = result;
|
|
307
|
+
task.progress = 100;
|
|
308
|
+
this._stats.completed++;
|
|
309
|
+
|
|
310
|
+
// Complete recording
|
|
311
|
+
if (this._replayEngine) {
|
|
312
|
+
this._replayEngine.completeRecording(task.id, result);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Mark complete in external queue
|
|
316
|
+
if (this._externalQueue && task._queueItemId) {
|
|
317
|
+
this._externalQueue.complete(task._queueItemId, result);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
bus.emit('task.completed', { taskId: task.id, duration: Date.now() - task.startedAt });
|
|
321
|
+
} catch (err) {
|
|
322
|
+
const isTimeout = err.message.includes('timed out');
|
|
323
|
+
if (isTimeout) this._stats.timedOut++;
|
|
324
|
+
|
|
325
|
+
if (task.retriesLeft > 0 && !isTimeout) {
|
|
326
|
+
// Retry with exponential backoff
|
|
327
|
+
task.retriesLeft--;
|
|
328
|
+
task.state = TaskState.RETRYING;
|
|
329
|
+
this._stats.retried++;
|
|
330
|
+
const backoff = Math.min(1000 * Math.pow(2, task.attempt - 1), 30000);
|
|
331
|
+
bus.emit('task.retrying', { taskId: task.id, attempt: task.attempt, backoff });
|
|
332
|
+
|
|
333
|
+
setTimeout(() => {
|
|
334
|
+
this._running.delete(task.id);
|
|
335
|
+
if (this._externalQueue) {
|
|
336
|
+
// Re-enqueue in external queue
|
|
337
|
+
this._externalQueue.enqueue({
|
|
338
|
+
type: task.type,
|
|
339
|
+
data: task,
|
|
340
|
+
priority: task.priority,
|
|
341
|
+
maxAttempts: task.retriesLeft + 1,
|
|
342
|
+
timeoutMs: task.timeout,
|
|
343
|
+
});
|
|
344
|
+
} else {
|
|
345
|
+
this._queue.unshift(task);
|
|
346
|
+
}
|
|
347
|
+
this._processQueue();
|
|
348
|
+
}, backoff);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
task.state = TaskState.FAILED;
|
|
353
|
+
task.error = { code: isTimeout ? 'TIMEOUT' : 'EXECUTION_ERROR', message: err.message };
|
|
354
|
+
this._stats.failed++;
|
|
355
|
+
|
|
356
|
+
// Complete recording with error
|
|
357
|
+
if (this._replayEngine) {
|
|
358
|
+
this._replayEngine.completeRecording(task.id, null, err);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Mark failed in external queue
|
|
362
|
+
if (this._externalQueue && task._queueItemId) {
|
|
363
|
+
this._externalQueue.fail(task._queueItemId, err.message);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
bus.emit('task.failed', { taskId: task.id, error: err.message, attempts: task.attempt });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
this._finishTask(task);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Finish a task (move to completed buffer)
|
|
374
|
+
*/
|
|
375
|
+
_finishTask(task) {
|
|
376
|
+
task.completedAt = Date.now();
|
|
377
|
+
this._running.delete(task.id);
|
|
378
|
+
|
|
379
|
+
// Store in completed buffer
|
|
380
|
+
this._completed.set(task.id, task);
|
|
381
|
+
if (this._completed.size > this._maxCompleted) {
|
|
382
|
+
const oldest = this._completed.keys().next().value;
|
|
383
|
+
this._completed.delete(oldest);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Process more tasks
|
|
387
|
+
this._processQueue();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Get task status
|
|
392
|
+
*/
|
|
393
|
+
getTask(taskId) {
|
|
394
|
+
// Check running
|
|
395
|
+
let task = this._running.get(taskId);
|
|
396
|
+
if (!task) {
|
|
397
|
+
// Check queue
|
|
398
|
+
task = this._queue.find(t => t.id === taskId);
|
|
399
|
+
}
|
|
400
|
+
if (!task) {
|
|
401
|
+
// Check completed
|
|
402
|
+
task = this._completed.get(taskId);
|
|
403
|
+
}
|
|
404
|
+
if (!task) return null;
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
id: task.id,
|
|
408
|
+
type: task.type,
|
|
409
|
+
objective: task.objective,
|
|
410
|
+
state: task.state,
|
|
411
|
+
progress: task.progress,
|
|
412
|
+
currentStep: task.currentStep,
|
|
413
|
+
totalSteps: task.steps.length,
|
|
414
|
+
attempt: task.attempt,
|
|
415
|
+
result: task.result,
|
|
416
|
+
error: task.error,
|
|
417
|
+
checkpoints: task.checkpoints.length,
|
|
418
|
+
submittedAt: task.submittedAt,
|
|
419
|
+
startedAt: task.startedAt,
|
|
420
|
+
completedAt: task.completedAt,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Cancel a task
|
|
426
|
+
*/
|
|
427
|
+
cancel(taskId) {
|
|
428
|
+
// Remove from queue
|
|
429
|
+
const idx = this._queue.findIndex(t => t.id === taskId);
|
|
430
|
+
if (idx !== -1) {
|
|
431
|
+
const task = this._queue.splice(idx, 1)[0];
|
|
432
|
+
task.state = TaskState.CANCELLED;
|
|
433
|
+
task.completedAt = Date.now();
|
|
434
|
+
this._completed.set(task.id, task);
|
|
435
|
+
this._stats.cancelled++;
|
|
436
|
+
bus.emit('task.cancelled', { taskId, reason: 'user' });
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Mark running task as cancelled (handler must check)
|
|
441
|
+
const running = this._running.get(taskId);
|
|
442
|
+
if (running) {
|
|
443
|
+
running.state = TaskState.CANCELLED;
|
|
444
|
+
this._stats.cancelled++;
|
|
445
|
+
bus.emit('task.cancelled', { taskId, reason: 'user' });
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Pause a running task (handler must support this)
|
|
454
|
+
*/
|
|
455
|
+
pause(taskId) {
|
|
456
|
+
const task = this._running.get(taskId);
|
|
457
|
+
if (task && task.state === TaskState.RUNNING) {
|
|
458
|
+
task.state = TaskState.PAUSED;
|
|
459
|
+
bus.emit('task.paused', { taskId });
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Resume a paused task
|
|
467
|
+
*/
|
|
468
|
+
resume(taskId) {
|
|
469
|
+
const task = this._running.get(taskId);
|
|
470
|
+
if (task && task.state === TaskState.PAUSED) {
|
|
471
|
+
task.state = TaskState.RUNNING;
|
|
472
|
+
bus.emit('task.resumed', { taskId });
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Get scheduler stats
|
|
480
|
+
*/
|
|
481
|
+
getStats() {
|
|
482
|
+
const queueSize = this._externalQueue
|
|
483
|
+
? this._externalQueue.size()
|
|
484
|
+
: this._queue.length;
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
...this._stats,
|
|
488
|
+
queueSize,
|
|
489
|
+
queueBackend: this._externalQueue ? this._externalQueue.constructor.name : 'memory',
|
|
490
|
+
runningCount: this._running.size,
|
|
491
|
+
completedBufferSize: this._completed.size,
|
|
492
|
+
replayEnabled: !!this._replayEngine,
|
|
493
|
+
containerEnabled: !!this._containerRunner,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* List tasks by state
|
|
499
|
+
*/
|
|
500
|
+
listTasks(state, limit = 50) {
|
|
501
|
+
const tasks = [];
|
|
502
|
+
|
|
503
|
+
if (!state || state === TaskState.QUEUED) {
|
|
504
|
+
for (const t of this._queue.slice(0, limit)) {
|
|
505
|
+
tasks.push(this.getTask(t.id));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (!state || state === TaskState.RUNNING) {
|
|
509
|
+
for (const [, t] of this._running) {
|
|
510
|
+
if (tasks.length >= limit) break;
|
|
511
|
+
tasks.push(this.getTask(t.id));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (!state || [TaskState.COMPLETED, TaskState.FAILED, TaskState.CANCELLED].includes(state)) {
|
|
515
|
+
for (const [, t] of this._completed) {
|
|
516
|
+
if (tasks.length >= limit) break;
|
|
517
|
+
if (!state || t.state === state) tasks.push(this.getTask(t.id));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return tasks.slice(0, limit);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function _withTimeout(promise, ms) {
|
|
526
|
+
if (!ms || ms <= 0) return promise;
|
|
527
|
+
return new Promise((resolve, reject) => {
|
|
528
|
+
const timer = setTimeout(() => reject(new Error(`Task timed out after ${ms}ms`)), ms);
|
|
529
|
+
promise.then(r => { clearTimeout(timer); resolve(r); })
|
|
530
|
+
.catch(e => { clearTimeout(timer); reject(e); });
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
module.exports = { Scheduler, TaskState };
|