converse-mcp-server 2.3.1 → 2.4.1
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/README.md +771 -738
- package/docs/API.md +10 -1
- package/docs/PROVIDERS.md +8 -4
- package/package.json +12 -12
- package/src/async/asyncJobStore.js +82 -52
- package/src/async/eventBus.js +25 -20
- package/src/async/fileCache.js +121 -40
- package/src/async/jobRunner.js +65 -39
- package/src/async/providerStreamNormalizer.js +203 -117
- package/src/config.js +374 -102
- package/src/continuationStore.js +32 -24
- package/src/index.js +45 -25
- package/src/prompts/helpPrompt.js +328 -305
- package/src/providers/anthropic.js +303 -119
- package/src/providers/codex.js +103 -45
- package/src/providers/deepseek.js +24 -8
- package/src/providers/google.js +337 -93
- package/src/providers/index.js +1 -1
- package/src/providers/interface.js +16 -11
- package/src/providers/mistral.js +179 -69
- package/src/providers/openai-compatible.js +231 -94
- package/src/providers/openai.js +1094 -914
- package/src/providers/openrouter-endpoints-client.js +220 -216
- package/src/providers/openrouter.js +426 -381
- package/src/providers/xai.js +153 -56
- package/src/resources/helpResource.js +70 -67
- package/src/router.js +95 -67
- package/src/services/summarizationService.js +51 -24
- package/src/systemPrompts.js +89 -89
- package/src/tools/cancelJob.js +31 -19
- package/src/tools/chat.js +997 -883
- package/src/tools/checkStatus.js +86 -65
- package/src/tools/consensus.js +400 -234
- package/src/tools/index.js +39 -16
- package/src/transport/httpTransport.js +82 -55
- package/src/utils/contextProcessor.js +54 -37
- package/src/utils/errorHandler.js +95 -45
- package/src/utils/fileValidator.js +107 -98
- package/src/utils/formatStatus.js +122 -64
- package/src/utils/logger.js +459 -449
- package/src/utils/pathUtils.js +2 -2
- package/src/utils/tokenLimiter.js +216 -216
package/src/async/fileCache.js
CHANGED
|
@@ -34,7 +34,9 @@ export class FileCacheInterface {
|
|
|
34
34
|
* @returns {Promise<void>}
|
|
35
35
|
*/
|
|
36
36
|
async writeJournalEvent(_jobId, _event) {
|
|
37
|
-
throw new Error(
|
|
37
|
+
throw new Error(
|
|
38
|
+
'writeJournalEvent() method must be implemented by file cache backend',
|
|
39
|
+
);
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
/**
|
|
@@ -44,7 +46,9 @@ export class FileCacheInterface {
|
|
|
44
46
|
* @returns {Promise<void>}
|
|
45
47
|
*/
|
|
46
48
|
async writeSnapshot(_jobId, _result) {
|
|
47
|
-
throw new Error(
|
|
49
|
+
throw new Error(
|
|
50
|
+
'writeSnapshot() method must be implemented by file cache backend',
|
|
51
|
+
);
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
/**
|
|
@@ -53,7 +57,9 @@ export class FileCacheInterface {
|
|
|
53
57
|
* @returns {Promise<object|null>} Job result or null if not found
|
|
54
58
|
*/
|
|
55
59
|
async readSnapshot(_jobId) {
|
|
56
|
-
throw new Error(
|
|
60
|
+
throw new Error(
|
|
61
|
+
'readSnapshot() method must be implemented by file cache backend',
|
|
62
|
+
);
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
/**
|
|
@@ -62,7 +68,9 @@ export class FileCacheInterface {
|
|
|
62
68
|
* @returns {Promise<number>} Number of directories cleaned up
|
|
63
69
|
*/
|
|
64
70
|
async cleanup(_maxAgeMs = 3 * 24 * 60 * 60 * 1000) {
|
|
65
|
-
throw new Error(
|
|
71
|
+
throw new Error(
|
|
72
|
+
'cleanup() method must be implemented by file cache backend',
|
|
73
|
+
);
|
|
66
74
|
}
|
|
67
75
|
}
|
|
68
76
|
|
|
@@ -72,11 +80,16 @@ export class FileCacheInterface {
|
|
|
72
80
|
export class FileCache extends FileCacheInterface {
|
|
73
81
|
constructor(options = {}) {
|
|
74
82
|
super();
|
|
75
|
-
this.baseDir =
|
|
83
|
+
this.baseDir =
|
|
84
|
+
options.baseDir ||
|
|
85
|
+
process.env.ASYNC_CACHE_DIR ||
|
|
86
|
+
path.join(process.cwd(), 'cache', 'async');
|
|
76
87
|
this.cleanupInterval = options.cleanupInterval || 10 * 60 * 1000; // 10 minutes
|
|
77
88
|
|
|
78
89
|
// Check environment variable for disk TTL
|
|
79
|
-
const envDiskTTL = process.env.ASYNC_DISK_TTL_MS
|
|
90
|
+
const envDiskTTL = process.env.ASYNC_DISK_TTL_MS
|
|
91
|
+
? parseInt(process.env.ASYNC_DISK_TTL_MS, 10)
|
|
92
|
+
: null;
|
|
80
93
|
this.maxAge = options.maxAge || envDiskTTL || 3 * 24 * 60 * 60 * 1000; // 3 days default
|
|
81
94
|
|
|
82
95
|
this.cleanupTimer = null;
|
|
@@ -84,7 +97,10 @@ export class FileCache extends FileCacheInterface {
|
|
|
84
97
|
// Start cleanup timer
|
|
85
98
|
this.startCleanupTimer();
|
|
86
99
|
|
|
87
|
-
debugLog(
|
|
100
|
+
debugLog(
|
|
101
|
+
'FileCache',
|
|
102
|
+
`Initialized with baseDir: ${this.baseDir}, maxAge: ${this.maxAge}ms`,
|
|
103
|
+
);
|
|
88
104
|
}
|
|
89
105
|
|
|
90
106
|
/**
|
|
@@ -100,7 +116,10 @@ export class FileCache extends FileCacheInterface {
|
|
|
100
116
|
try {
|
|
101
117
|
const cleaned = await this.cleanup();
|
|
102
118
|
if (cleaned > 0) {
|
|
103
|
-
debugLog(
|
|
119
|
+
debugLog(
|
|
120
|
+
'FileCache',
|
|
121
|
+
`Cleanup completed: ${cleaned} directories removed`,
|
|
122
|
+
);
|
|
104
123
|
}
|
|
105
124
|
} catch (error) {
|
|
106
125
|
debugError('FileCache', 'Cleanup timer error:', error);
|
|
@@ -167,7 +186,7 @@ export class FileCache extends FileCacheInterface {
|
|
|
167
186
|
throw new FileCacheError(
|
|
168
187
|
`Failed to create directory: ${dirPath}`,
|
|
169
188
|
ERROR_CODES.CACHE_DIRECTORY_CREATION_FAILED,
|
|
170
|
-
{ dirPath, originalError: error.message }
|
|
189
|
+
{ dirPath, originalError: error.message },
|
|
171
190
|
);
|
|
172
191
|
}
|
|
173
192
|
}
|
|
@@ -183,7 +202,7 @@ export class FileCache extends FileCacheInterface {
|
|
|
183
202
|
throw new FileCacheError(
|
|
184
203
|
'Job ID must be a non-empty string',
|
|
185
204
|
ERROR_CODES.CACHE_WRITE_FAILED,
|
|
186
|
-
{ jobId }
|
|
205
|
+
{ jobId },
|
|
187
206
|
);
|
|
188
207
|
}
|
|
189
208
|
|
|
@@ -191,7 +210,7 @@ export class FileCache extends FileCacheInterface {
|
|
|
191
210
|
throw new FileCacheError(
|
|
192
211
|
'Event must be an object',
|
|
193
212
|
ERROR_CODES.CACHE_WRITE_FAILED,
|
|
194
|
-
{ jobId, event }
|
|
213
|
+
{ jobId, event },
|
|
195
214
|
);
|
|
196
215
|
}
|
|
197
216
|
|
|
@@ -206,24 +225,32 @@ export class FileCache extends FileCacheInterface {
|
|
|
206
225
|
const eventWithMeta = {
|
|
207
226
|
ts: Date.now(),
|
|
208
227
|
jobId,
|
|
209
|
-
...event
|
|
228
|
+
...event,
|
|
210
229
|
};
|
|
211
230
|
|
|
212
231
|
// Append NDJSON line
|
|
213
232
|
const ndjsonLine = JSON.stringify(eventWithMeta) + '\n';
|
|
214
233
|
await fs.appendFile(journalPath, ndjsonLine, 'utf8');
|
|
215
234
|
|
|
216
|
-
debugLog(
|
|
235
|
+
debugLog(
|
|
236
|
+
'FileCache',
|
|
237
|
+
`Journal event written for job ${jobId}:`,
|
|
238
|
+
event.type || 'unknown',
|
|
239
|
+
);
|
|
217
240
|
} catch (error) {
|
|
218
241
|
if (error instanceof FileCacheError) {
|
|
219
242
|
throw error;
|
|
220
243
|
}
|
|
221
244
|
|
|
222
|
-
debugError(
|
|
245
|
+
debugError(
|
|
246
|
+
'FileCache',
|
|
247
|
+
`Failed to write journal event for job ${jobId}:`,
|
|
248
|
+
error,
|
|
249
|
+
);
|
|
223
250
|
throw new FileCacheError(
|
|
224
251
|
`Failed to write journal event for job ${jobId}`,
|
|
225
252
|
ERROR_CODES.CACHE_WRITE_FAILED,
|
|
226
|
-
{ jobId, event, originalError: error.message }
|
|
253
|
+
{ jobId, event, originalError: error.message },
|
|
227
254
|
);
|
|
228
255
|
}
|
|
229
256
|
}
|
|
@@ -239,7 +266,7 @@ export class FileCache extends FileCacheInterface {
|
|
|
239
266
|
throw new FileCacheError(
|
|
240
267
|
'Job ID must be a non-empty string',
|
|
241
268
|
ERROR_CODES.CACHE_WRITE_FAILED,
|
|
242
|
-
{ jobId }
|
|
269
|
+
{ jobId },
|
|
243
270
|
);
|
|
244
271
|
}
|
|
245
272
|
|
|
@@ -247,7 +274,7 @@ export class FileCache extends FileCacheInterface {
|
|
|
247
274
|
throw new FileCacheError(
|
|
248
275
|
'Result must be an object',
|
|
249
276
|
ERROR_CODES.CACHE_WRITE_FAILED,
|
|
250
|
-
{ jobId, result }
|
|
277
|
+
{ jobId, result },
|
|
251
278
|
);
|
|
252
279
|
}
|
|
253
280
|
|
|
@@ -262,7 +289,7 @@ export class FileCache extends FileCacheInterface {
|
|
|
262
289
|
const snapshot = {
|
|
263
290
|
jobId,
|
|
264
291
|
completedAt: Date.now(),
|
|
265
|
-
...result
|
|
292
|
+
...result,
|
|
266
293
|
};
|
|
267
294
|
|
|
268
295
|
// Write pretty-printed JSON
|
|
@@ -275,11 +302,15 @@ export class FileCache extends FileCacheInterface {
|
|
|
275
302
|
throw error;
|
|
276
303
|
}
|
|
277
304
|
|
|
278
|
-
debugError(
|
|
305
|
+
debugError(
|
|
306
|
+
'FileCache',
|
|
307
|
+
`Failed to write snapshot for job ${jobId}:`,
|
|
308
|
+
error,
|
|
309
|
+
);
|
|
279
310
|
throw new FileCacheError(
|
|
280
311
|
`Failed to write snapshot for job ${jobId}`,
|
|
281
312
|
ERROR_CODES.CACHE_WRITE_FAILED,
|
|
282
|
-
{ jobId, result, originalError: error.message }
|
|
313
|
+
{ jobId, result, originalError: error.message },
|
|
283
314
|
);
|
|
284
315
|
}
|
|
285
316
|
}
|
|
@@ -294,7 +325,7 @@ export class FileCache extends FileCacheInterface {
|
|
|
294
325
|
throw new FileCacheError(
|
|
295
326
|
'Job ID must be a non-empty string',
|
|
296
327
|
ERROR_CODES.CACHE_READ_FAILED,
|
|
297
|
-
{ jobId }
|
|
328
|
+
{ jobId },
|
|
298
329
|
);
|
|
299
330
|
}
|
|
300
331
|
|
|
@@ -311,12 +342,18 @@ export class FileCache extends FileCacheInterface {
|
|
|
311
342
|
const lastUpdate = snapshot.updated_at || snapshot.ended_at;
|
|
312
343
|
const age = Date.now() - lastUpdate;
|
|
313
344
|
if (age > this.maxAge) {
|
|
314
|
-
debugLog(
|
|
345
|
+
debugLog(
|
|
346
|
+
'FileCache',
|
|
347
|
+
`Snapshot for job ${jobId} has expired (age: ${age}ms, maxAge: ${this.maxAge}ms)`,
|
|
348
|
+
);
|
|
315
349
|
return null;
|
|
316
350
|
}
|
|
317
351
|
}
|
|
318
352
|
|
|
319
|
-
debugLog(
|
|
353
|
+
debugLog(
|
|
354
|
+
'FileCache',
|
|
355
|
+
`Snapshot read for job ${jobId} from current date`,
|
|
356
|
+
);
|
|
320
357
|
return snapshot;
|
|
321
358
|
} catch (_currentDateError) {
|
|
322
359
|
// If not found in current date, search in recent directories
|
|
@@ -327,12 +364,18 @@ export class FileCache extends FileCacheInterface {
|
|
|
327
364
|
const lastUpdate = result.updated_at || result.ended_at;
|
|
328
365
|
const age = Date.now() - lastUpdate;
|
|
329
366
|
if (age > this.maxAge) {
|
|
330
|
-
debugLog(
|
|
367
|
+
debugLog(
|
|
368
|
+
'FileCache',
|
|
369
|
+
`Snapshot for job ${jobId} has expired (age: ${age}ms, maxAge: ${this.maxAge}ms)`,
|
|
370
|
+
);
|
|
331
371
|
return null;
|
|
332
372
|
}
|
|
333
373
|
}
|
|
334
374
|
|
|
335
|
-
debugLog(
|
|
375
|
+
debugLog(
|
|
376
|
+
'FileCache',
|
|
377
|
+
`Snapshot read for job ${jobId} from recent directories`,
|
|
378
|
+
);
|
|
336
379
|
return result;
|
|
337
380
|
}
|
|
338
381
|
|
|
@@ -345,11 +388,15 @@ export class FileCache extends FileCacheInterface {
|
|
|
345
388
|
throw error;
|
|
346
389
|
}
|
|
347
390
|
|
|
348
|
-
debugError(
|
|
391
|
+
debugError(
|
|
392
|
+
'FileCache',
|
|
393
|
+
`Failed to read snapshot for job ${jobId}:`,
|
|
394
|
+
error,
|
|
395
|
+
);
|
|
349
396
|
throw new FileCacheError(
|
|
350
397
|
`Failed to read snapshot for job ${jobId}`,
|
|
351
398
|
ERROR_CODES.CACHE_READ_FAILED,
|
|
352
|
-
{ jobId, originalError: error.message }
|
|
399
|
+
{ jobId, originalError: error.message },
|
|
353
400
|
);
|
|
354
401
|
}
|
|
355
402
|
}
|
|
@@ -365,12 +412,20 @@ export class FileCache extends FileCacheInterface {
|
|
|
365
412
|
// Get directories in base directory
|
|
366
413
|
const entries = await fs.readdir(this.baseDir, { withFileTypes: true });
|
|
367
414
|
const dateDirs = entries
|
|
368
|
-
.filter(
|
|
415
|
+
.filter(
|
|
416
|
+
(entry) =>
|
|
417
|
+
entry.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(entry.name),
|
|
418
|
+
)
|
|
369
419
|
.sort((a, b) => b.name.localeCompare(a.name)) // Most recent first
|
|
370
420
|
.slice(0, 10); // Only check last 10 days
|
|
371
421
|
|
|
372
422
|
for (const dateDir of dateDirs) {
|
|
373
|
-
const snapshotPath = path.join(
|
|
423
|
+
const snapshotPath = path.join(
|
|
424
|
+
this.baseDir,
|
|
425
|
+
dateDir.name,
|
|
426
|
+
jobId,
|
|
427
|
+
'result.json',
|
|
428
|
+
);
|
|
374
429
|
try {
|
|
375
430
|
const content = await fs.readFile(snapshotPath, 'utf8');
|
|
376
431
|
return JSON.parse(content);
|
|
@@ -409,7 +464,10 @@ export class FileCache extends FileCacheInterface {
|
|
|
409
464
|
// Get directories in base directory
|
|
410
465
|
const entries = await fs.readdir(this.baseDir, { withFileTypes: true });
|
|
411
466
|
const dateDirs = entries
|
|
412
|
-
.filter(
|
|
467
|
+
.filter(
|
|
468
|
+
(entry) =>
|
|
469
|
+
entry.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(entry.name),
|
|
470
|
+
)
|
|
413
471
|
.sort((a, b) => b.name.localeCompare(a.name)) // Most recent first
|
|
414
472
|
.slice(0, daysBack); // Only check specified days back
|
|
415
473
|
|
|
@@ -421,9 +479,11 @@ export class FileCache extends FileCacheInterface {
|
|
|
421
479
|
|
|
422
480
|
try {
|
|
423
481
|
// Get all job directories in this date
|
|
424
|
-
const jobEntries = await fs.readdir(dateDirPath, {
|
|
482
|
+
const jobEntries = await fs.readdir(dateDirPath, {
|
|
483
|
+
withFileTypes: true,
|
|
484
|
+
});
|
|
425
485
|
const jobDirs = jobEntries
|
|
426
|
-
.filter(entry => entry.isDirectory())
|
|
486
|
+
.filter((entry) => entry.isDirectory())
|
|
427
487
|
.sort((a, b) => {
|
|
428
488
|
// Try to sort by modification time if possible
|
|
429
489
|
try {
|
|
@@ -441,14 +501,23 @@ export class FileCache extends FileCacheInterface {
|
|
|
441
501
|
for (const jobDir of jobDirs) {
|
|
442
502
|
if (jobs.length >= limit) break;
|
|
443
503
|
|
|
444
|
-
const snapshotPath = path.join(
|
|
504
|
+
const snapshotPath = path.join(
|
|
505
|
+
dateDirPath,
|
|
506
|
+
jobDir.name,
|
|
507
|
+
'result.json',
|
|
508
|
+
);
|
|
445
509
|
|
|
446
510
|
try {
|
|
447
511
|
const content = await fs.readFile(snapshotPath, 'utf8');
|
|
448
512
|
const snapshot = JSON.parse(content);
|
|
449
513
|
|
|
450
514
|
// Add the job to our list if it's completed
|
|
451
|
-
if (
|
|
515
|
+
if (
|
|
516
|
+
snapshot &&
|
|
517
|
+
(snapshot.status === 'completed' ||
|
|
518
|
+
snapshot.status === 'failed' ||
|
|
519
|
+
snapshot.status === 'cancelled')
|
|
520
|
+
) {
|
|
452
521
|
jobs.push(snapshot);
|
|
453
522
|
}
|
|
454
523
|
} catch {
|
|
@@ -457,7 +526,11 @@ export class FileCache extends FileCacheInterface {
|
|
|
457
526
|
}
|
|
458
527
|
}
|
|
459
528
|
} catch (error) {
|
|
460
|
-
debugError(
|
|
529
|
+
debugError(
|
|
530
|
+
'FileCache',
|
|
531
|
+
`Failed to read date directory ${dateDir.name}:`,
|
|
532
|
+
error,
|
|
533
|
+
);
|
|
461
534
|
// Continue with other directories
|
|
462
535
|
}
|
|
463
536
|
}
|
|
@@ -486,14 +559,18 @@ export class FileCache extends FileCacheInterface {
|
|
|
486
559
|
await fs.access(this.baseDir);
|
|
487
560
|
} catch {
|
|
488
561
|
// Base directory doesn't exist, nothing to clean
|
|
489
|
-
debugLog(
|
|
562
|
+
debugLog(
|
|
563
|
+
'FileCache',
|
|
564
|
+
'Base directory does not exist, skipping cleanup',
|
|
565
|
+
);
|
|
490
566
|
return 0;
|
|
491
567
|
}
|
|
492
568
|
|
|
493
569
|
// Get all date directories
|
|
494
570
|
const entries = await fs.readdir(this.baseDir, { withFileTypes: true });
|
|
495
|
-
const dateDirs = entries.filter(
|
|
496
|
-
entry
|
|
571
|
+
const dateDirs = entries.filter(
|
|
572
|
+
(entry) =>
|
|
573
|
+
entry.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(entry.name),
|
|
497
574
|
);
|
|
498
575
|
|
|
499
576
|
for (const dateDir of dateDirs) {
|
|
@@ -509,7 +586,11 @@ export class FileCache extends FileCacheInterface {
|
|
|
509
586
|
debugLog('FileCache', `Cleaned up old directory: ${dateDir.name}`);
|
|
510
587
|
}
|
|
511
588
|
} catch (error) {
|
|
512
|
-
debugError(
|
|
589
|
+
debugError(
|
|
590
|
+
'FileCache',
|
|
591
|
+
`Failed to clean directory ${dateDir.name}:`,
|
|
592
|
+
error,
|
|
593
|
+
);
|
|
513
594
|
// Continue with other directories
|
|
514
595
|
}
|
|
515
596
|
}
|
|
@@ -520,7 +601,7 @@ export class FileCache extends FileCacheInterface {
|
|
|
520
601
|
throw new FileCacheError(
|
|
521
602
|
'Failed to cleanup old cache directories',
|
|
522
603
|
ERROR_CODES.CACHE_CLEANUP_FAILED,
|
|
523
|
-
{ maxAgeMs, originalError: error.message }
|
|
604
|
+
{ maxAgeMs, originalError: error.message },
|
|
524
605
|
);
|
|
525
606
|
}
|
|
526
607
|
}
|
package/src/async/jobRunner.js
CHANGED
|
@@ -44,7 +44,7 @@ export class JobRunner extends EventEmitter {
|
|
|
44
44
|
if (!dependencies || !dependencies.asyncJobStore) {
|
|
45
45
|
throw new JobRunnerError(
|
|
46
46
|
'AsyncJobStore is required',
|
|
47
|
-
'MISSING_DEPENDENCIES'
|
|
47
|
+
'MISSING_DEPENDENCIES',
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -72,7 +72,9 @@ export class JobRunner extends EventEmitter {
|
|
|
72
72
|
activeCount: 0,
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
-
debugLog(
|
|
75
|
+
debugLog(
|
|
76
|
+
`JobRunner: Initialized with concurrency limit ${this.concurrency}`,
|
|
77
|
+
);
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
/**
|
|
@@ -96,27 +98,24 @@ export class JobRunner extends EventEmitter {
|
|
|
96
98
|
if (!jobSpec || !jobSpec.tool) {
|
|
97
99
|
throw new JobRunnerError(
|
|
98
100
|
'Invalid job specification: tool is required',
|
|
99
|
-
'INVALID_JOB_SPEC'
|
|
101
|
+
'INVALID_JOB_SPEC',
|
|
100
102
|
);
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
if (typeof runFunction !== 'function') {
|
|
104
106
|
throw new JobRunnerError(
|
|
105
107
|
'runFunction must be a callable function',
|
|
106
|
-
'INVALID_RUN_FUNCTION'
|
|
108
|
+
'INVALID_RUN_FUNCTION',
|
|
107
109
|
);
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
// Create job in store
|
|
111
|
-
const jobId = await this.asyncJobStore.create(
|
|
112
|
-
jobSpec.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
...jobSpec.options,
|
|
118
|
-
}
|
|
119
|
-
);
|
|
113
|
+
const jobId = await this.asyncJobStore.create(jobSpec.tool, {
|
|
114
|
+
sessionId: jobSpec.sessionId,
|
|
115
|
+
timeout: options.timeout || this.defaultTimeout,
|
|
116
|
+
priority: options.priority || false,
|
|
117
|
+
...jobSpec.options,
|
|
118
|
+
});
|
|
120
119
|
|
|
121
120
|
// Set up abort controller for timeout and cancellation
|
|
122
121
|
const abortController = new globalThis.AbortController();
|
|
@@ -138,13 +137,16 @@ export class JobRunner extends EventEmitter {
|
|
|
138
137
|
|
|
139
138
|
// Submit to limiter queue for background execution
|
|
140
139
|
const limitedExecution = this.limiter(() =>
|
|
141
|
-
this._executeJob(jobId, runFunction, options, abortController.signal)
|
|
140
|
+
this._executeJob(jobId, runFunction, options, abortController.signal),
|
|
142
141
|
);
|
|
143
142
|
|
|
144
143
|
// Don't await - execute in background
|
|
145
144
|
globalThis.setImmediate(() => {
|
|
146
145
|
limitedExecution.catch((error) => {
|
|
147
|
-
debugError(
|
|
146
|
+
debugError(
|
|
147
|
+
`JobRunner: Background execution failed for ${jobId}:`,
|
|
148
|
+
error,
|
|
149
|
+
);
|
|
148
150
|
});
|
|
149
151
|
});
|
|
150
152
|
|
|
@@ -154,14 +156,13 @@ export class JobRunner extends EventEmitter {
|
|
|
154
156
|
|
|
155
157
|
debugLog(`JobRunner: Submitted job ${jobId} for background execution`);
|
|
156
158
|
return jobId;
|
|
157
|
-
|
|
158
159
|
} catch (error) {
|
|
159
160
|
if (error instanceof JobRunnerError) {
|
|
160
161
|
throw error;
|
|
161
162
|
}
|
|
162
163
|
throw new JobRunnerError(
|
|
163
164
|
`Failed to submit job: ${error.message}`,
|
|
164
|
-
'SUBMISSION_ERROR'
|
|
165
|
+
'SUBMISSION_ERROR',
|
|
165
166
|
);
|
|
166
167
|
}
|
|
167
168
|
}
|
|
@@ -215,7 +216,6 @@ export class JobRunner extends EventEmitter {
|
|
|
215
216
|
|
|
216
217
|
debugLog(`JobRunner: Cancelled job ${jobId}`);
|
|
217
218
|
return true;
|
|
218
|
-
|
|
219
219
|
} catch (error) {
|
|
220
220
|
debugError(`JobRunner: Failed to cancel job ${jobId}:`, error);
|
|
221
221
|
return false;
|
|
@@ -251,7 +251,10 @@ export class JobRunner extends EventEmitter {
|
|
|
251
251
|
// Get current job state
|
|
252
252
|
jobState = await this.asyncJobStore.get(jobId);
|
|
253
253
|
if (!jobState) {
|
|
254
|
-
throw new JobRunnerError(
|
|
254
|
+
throw new JobRunnerError(
|
|
255
|
+
`Job ${jobId} not found in store`,
|
|
256
|
+
'JOB_NOT_FOUND',
|
|
257
|
+
);
|
|
255
258
|
}
|
|
256
259
|
|
|
257
260
|
// Check if already cancelled or aborted
|
|
@@ -269,7 +272,7 @@ export class JobRunner extends EventEmitter {
|
|
|
269
272
|
|
|
270
273
|
this.activeJobs.set(jobId, {
|
|
271
274
|
startedAt: Date.now(),
|
|
272
|
-
tool: jobState.tool
|
|
275
|
+
tool: jobState.tool,
|
|
273
276
|
});
|
|
274
277
|
|
|
275
278
|
// Emit job started event through EventBus
|
|
@@ -289,7 +292,9 @@ export class JobRunner extends EventEmitter {
|
|
|
289
292
|
const timeoutHandle = setTimeout(() => {
|
|
290
293
|
if (!signal.aborted) {
|
|
291
294
|
debugLog(`JobRunner: Job ${jobId} timed out after ${timeout}ms`);
|
|
292
|
-
this.abortControllers
|
|
295
|
+
this.abortControllers
|
|
296
|
+
.get(jobId)
|
|
297
|
+
?.abort(`Job timed out after ${timeout}ms`);
|
|
293
298
|
}
|
|
294
299
|
}, timeout);
|
|
295
300
|
|
|
@@ -300,7 +305,8 @@ export class JobRunner extends EventEmitter {
|
|
|
300
305
|
tool: jobState.tool,
|
|
301
306
|
signal,
|
|
302
307
|
updateJob: (updates) => this.asyncJobStore.update(jobId, updates),
|
|
303
|
-
emitEvent: (eventType, data) =>
|
|
308
|
+
emitEvent: (eventType, data) =>
|
|
309
|
+
this.emit(eventType, { jobId, ...data }),
|
|
304
310
|
};
|
|
305
311
|
|
|
306
312
|
// Execute the job function
|
|
@@ -337,11 +343,16 @@ export class JobRunner extends EventEmitter {
|
|
|
337
343
|
provider: finalJobState.provider,
|
|
338
344
|
model: finalJobState.model,
|
|
339
345
|
title: finalJobState.title,
|
|
340
|
-
final_summary: finalJobState.final_summary
|
|
346
|
+
final_summary: finalJobState.final_summary,
|
|
341
347
|
});
|
|
342
|
-
debugLog(
|
|
348
|
+
debugLog(
|
|
349
|
+
`JobRunner: Written snapshot to FileCache for completed job ${jobId}`,
|
|
350
|
+
);
|
|
343
351
|
} catch (cacheError) {
|
|
344
|
-
debugError(
|
|
352
|
+
debugError(
|
|
353
|
+
`JobRunner: Failed to write to FileCache for job ${jobId}:`,
|
|
354
|
+
cacheError,
|
|
355
|
+
);
|
|
345
356
|
// Continue even if cache write fails
|
|
346
357
|
}
|
|
347
358
|
}
|
|
@@ -359,7 +370,6 @@ export class JobRunner extends EventEmitter {
|
|
|
359
370
|
|
|
360
371
|
this.stats.completed++;
|
|
361
372
|
debugLog(`JobRunner: Completed job ${jobId}`);
|
|
362
|
-
|
|
363
373
|
} catch (executionError) {
|
|
364
374
|
// Clear timeout
|
|
365
375
|
clearTimeout(timeoutHandle);
|
|
@@ -392,11 +402,16 @@ export class JobRunner extends EventEmitter {
|
|
|
392
402
|
startedAt: cancelledJobState.startedAt,
|
|
393
403
|
provider: cancelledJobState.provider,
|
|
394
404
|
model: cancelledJobState.model,
|
|
395
|
-
title: cancelledJobState.title
|
|
405
|
+
title: cancelledJobState.title,
|
|
396
406
|
});
|
|
397
|
-
debugLog(
|
|
407
|
+
debugLog(
|
|
408
|
+
`JobRunner: Written snapshot to FileCache for cancelled job ${jobId}`,
|
|
409
|
+
);
|
|
398
410
|
} catch (cacheError) {
|
|
399
|
-
debugError(
|
|
411
|
+
debugError(
|
|
412
|
+
`JobRunner: Failed to write to FileCache for job ${jobId}:`,
|
|
413
|
+
cacheError,
|
|
414
|
+
);
|
|
400
415
|
// Continue even if cache write fails
|
|
401
416
|
}
|
|
402
417
|
}
|
|
@@ -437,11 +452,16 @@ export class JobRunner extends EventEmitter {
|
|
|
437
452
|
startedAt: failedJobState.startedAt,
|
|
438
453
|
provider: failedJobState.provider,
|
|
439
454
|
model: failedJobState.model,
|
|
440
|
-
title: failedJobState.title
|
|
455
|
+
title: failedJobState.title,
|
|
441
456
|
});
|
|
442
|
-
debugLog(
|
|
457
|
+
debugLog(
|
|
458
|
+
`JobRunner: Written snapshot to FileCache for failed job ${jobId}`,
|
|
459
|
+
);
|
|
443
460
|
} catch (cacheError) {
|
|
444
|
-
debugError(
|
|
461
|
+
debugError(
|
|
462
|
+
`JobRunner: Failed to write to FileCache for job ${jobId}:`,
|
|
463
|
+
cacheError,
|
|
464
|
+
);
|
|
445
465
|
// Continue even if cache write fails
|
|
446
466
|
}
|
|
447
467
|
}
|
|
@@ -460,7 +480,6 @@ export class JobRunner extends EventEmitter {
|
|
|
460
480
|
this.stats.failed++;
|
|
461
481
|
debugError(`JobRunner: Job ${jobId} failed:`, executionError);
|
|
462
482
|
}
|
|
463
|
-
|
|
464
483
|
} catch (error) {
|
|
465
484
|
// Handle system-level errors
|
|
466
485
|
debugError('JobRunner: System error during job execution:', error);
|
|
@@ -481,12 +500,14 @@ export class JobRunner extends EventEmitter {
|
|
|
481
500
|
timestamp: Date.now(),
|
|
482
501
|
});
|
|
483
502
|
} catch (updateError) {
|
|
484
|
-
debugError(
|
|
503
|
+
debugError(
|
|
504
|
+
`JobRunner: Failed to update job ${jobId} after system error:`,
|
|
505
|
+
updateError,
|
|
506
|
+
);
|
|
485
507
|
}
|
|
486
508
|
}
|
|
487
509
|
|
|
488
510
|
this.stats.failed++;
|
|
489
|
-
|
|
490
511
|
} finally {
|
|
491
512
|
// Clean up tracking
|
|
492
513
|
this.activeJobs.delete(jobId);
|
|
@@ -510,12 +531,17 @@ export class JobRunner extends EventEmitter {
|
|
|
510
531
|
|
|
511
532
|
// Wait for active jobs to complete or timeout
|
|
512
533
|
const shutdownStart = Date.now();
|
|
513
|
-
while (
|
|
514
|
-
|
|
534
|
+
while (
|
|
535
|
+
this.stats.activeCount > 0 &&
|
|
536
|
+
Date.now() - shutdownStart < timeoutMs
|
|
537
|
+
) {
|
|
538
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
515
539
|
}
|
|
516
540
|
|
|
517
541
|
if (this.stats.activeCount > 0) {
|
|
518
|
-
debugLog(
|
|
542
|
+
debugLog(
|
|
543
|
+
`JobRunner: Forced shutdown - ${this.stats.activeCount} jobs still active`,
|
|
544
|
+
);
|
|
519
545
|
}
|
|
520
546
|
|
|
521
547
|
// Clear all tracking
|
|
@@ -561,7 +587,7 @@ export function setJobRunner(runner) {
|
|
|
561
587
|
if (runner !== null && !(runner instanceof JobRunner)) {
|
|
562
588
|
throw new JobRunnerError(
|
|
563
589
|
'Runner must be a JobRunner instance',
|
|
564
|
-
'INVALID_RUNNER'
|
|
590
|
+
'INVALID_RUNNER',
|
|
565
591
|
);
|
|
566
592
|
}
|
|
567
593
|
globalJobRunner = runner;
|