knoxis-helper 1.4.5 → 1.4.6
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/lib/knoxis-pair-program.js +355 -22
- package/package.json +1 -1
|
@@ -5,6 +5,24 @@ const path = require('path');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const { spawn, spawnSync, execSync } = require('child_process');
|
|
7
7
|
|
|
8
|
+
// ===== RETRY CONFIGURATION =====
|
|
9
|
+
// Can be overridden via environment variables
|
|
10
|
+
const RETRY_CONFIG = {
|
|
11
|
+
maxRetries: parseInt(process.env.KNOXIS_MAX_RETRIES || '3'),
|
|
12
|
+
retryDelay: parseInt(process.env.KNOXIS_RETRY_DELAY || '30000'), // 30 seconds
|
|
13
|
+
gitTimeout: parseInt(process.env.KNOXIS_GIT_TIMEOUT || '30000'), // 30 seconds
|
|
14
|
+
aiCallTimeout: parseInt(process.env.KNOXIS_AI_TIMEOUT || '300000'), // 5 minutes
|
|
15
|
+
enableRetryLogging: process.env.KNOXIS_RETRY_LOG !== 'false'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Global retry statistics
|
|
19
|
+
let retryStats = {
|
|
20
|
+
totalRetries: 0,
|
|
21
|
+
successfulRetries: 0,
|
|
22
|
+
failedCommands: [],
|
|
23
|
+
startTime: Date.now()
|
|
24
|
+
};
|
|
25
|
+
|
|
8
26
|
function parseArgs(argv) {
|
|
9
27
|
const args = {};
|
|
10
28
|
const multi = {};
|
|
@@ -59,7 +77,45 @@ function commandExists(cmd) {
|
|
|
59
77
|
return result.status === 0;
|
|
60
78
|
}
|
|
61
79
|
|
|
62
|
-
// Resolve workspace name to path using knoxis registry
|
|
80
|
+
// Resolve workspace name to path using knoxis registry (async version)
|
|
81
|
+
async function resolveWorkspacePathAsync(nameOrPath) {
|
|
82
|
+
const os = require('os');
|
|
83
|
+
|
|
84
|
+
// Direct path - check if exists
|
|
85
|
+
try {
|
|
86
|
+
await fs.promises.stat(nameOrPath);
|
|
87
|
+
return path.resolve(nameOrPath);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
// Not a direct path, continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Try knoxis workspace registry
|
|
93
|
+
const workspacesFile = path.join(os.homedir(), '.knoxis', 'workspaces.json');
|
|
94
|
+
try {
|
|
95
|
+
const data = await fs.promises.readFile(workspacesFile, 'utf8');
|
|
96
|
+
const workspaces = JSON.parse(data);
|
|
97
|
+
|
|
98
|
+
// Exact match
|
|
99
|
+
if (workspaces[nameOrPath]) {
|
|
100
|
+
return workspaces[nameOrPath];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Fuzzy match
|
|
104
|
+
const lower = nameOrPath.toLowerCase();
|
|
105
|
+
for (const [name, wsPath] of Object.entries(workspaces)) {
|
|
106
|
+
if (name.toLowerCase().includes(lower)) {
|
|
107
|
+
console.log(`Matched workspace: ${name} -> ${wsPath}`);
|
|
108
|
+
return wsPath;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
// Registry file not found or parse error
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Sync version kept for backward compatibility
|
|
63
119
|
function resolveWorkspacePath(nameOrPath) {
|
|
64
120
|
const os = require('os');
|
|
65
121
|
|
|
@@ -132,32 +188,62 @@ function toArray(value) {
|
|
|
132
188
|
return [value];
|
|
133
189
|
}
|
|
134
190
|
|
|
135
|
-
function gatherContext(workspace, inputs) {
|
|
191
|
+
async function gatherContext(workspace, inputs) {
|
|
136
192
|
const sections = [];
|
|
137
193
|
const labels = [];
|
|
138
194
|
const seen = new Set();
|
|
195
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB limit
|
|
196
|
+
const ALLOWED_EXTENSIONS = /\.(js|jsx|ts|tsx|java|py|json|md|txt|yml|yaml|xml|html|css|scss|sql|sh|bash|env|config|conf)$/i;
|
|
139
197
|
|
|
140
|
-
toArray(inputs)
|
|
198
|
+
for (const entry of toArray(inputs)) {
|
|
141
199
|
if (typeof entry !== 'string') {
|
|
142
|
-
|
|
200
|
+
continue;
|
|
143
201
|
}
|
|
144
202
|
const trimmed = entry.trim();
|
|
145
203
|
if (!trimmed) {
|
|
146
|
-
|
|
204
|
+
continue;
|
|
147
205
|
}
|
|
148
206
|
const absolute = path.isAbsolute(trimmed) ? trimmed : path.join(workspace, trimmed);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const stats = await fs.promises.stat(absolute);
|
|
210
|
+
|
|
211
|
+
// Skip directories
|
|
212
|
+
if (stats.isDirectory()) {
|
|
213
|
+
console.warn(`[Context] Skipping directory: ${absolute}`);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Skip if already processed
|
|
218
|
+
if (seen.has(absolute)) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Check file size
|
|
223
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
224
|
+
console.warn(`[Context] Skipping large file (${(stats.size/1048576).toFixed(2)}MB): ${absolute}`);
|
|
225
|
+
labels.push(path.basename(absolute) + ' [too large]');
|
|
226
|
+
sections.push(formatSection(path.basename(absolute), `[File too large: ${(stats.size/1048576).toFixed(2)}MB]`));
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check file extension
|
|
231
|
+
if (!ALLOWED_EXTENSIONS.test(absolute)) {
|
|
232
|
+
console.warn(`[Context] Skipping non-text file: ${absolute}`);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
seen.add(absolute);
|
|
237
|
+
const content = await fs.promises.readFile(absolute, 'utf8');
|
|
238
|
+
const title = path.relative(workspace, absolute) || path.basename(absolute);
|
|
239
|
+
labels.push(title);
|
|
240
|
+
sections.push(formatSection(title, content));
|
|
241
|
+
} catch (err) {
|
|
242
|
+
if (err.code !== 'ENOENT') {
|
|
243
|
+
console.error(`[Context] Error reading ${absolute}: ${err.message}`);
|
|
244
|
+
}
|
|
154
245
|
}
|
|
155
|
-
|
|
156
|
-
const content = fs.readFileSync(absolute, 'utf8');
|
|
157
|
-
const title = path.relative(workspace, absolute) || path.basename(absolute);
|
|
158
|
-
labels.push(title);
|
|
159
|
-
sections.push(formatSection(title, content));
|
|
160
|
-
});
|
|
246
|
+
}
|
|
161
247
|
|
|
162
248
|
return { sections, labels };
|
|
163
249
|
}
|
|
@@ -309,7 +395,7 @@ function buildPrompt(options) {
|
|
|
309
395
|
return sections.join('\n\n');
|
|
310
396
|
}
|
|
311
397
|
|
|
312
|
-
async function
|
|
398
|
+
async function callAiBase(aiConfig, prompt, livePrinter) {
|
|
313
399
|
return new Promise((resolve, reject) => {
|
|
314
400
|
const proc = spawn(aiConfig.cmd, aiConfig.args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
315
401
|
let stdout = '';
|
|
@@ -350,6 +436,60 @@ async function callAi(aiConfig, prompt, livePrinter) {
|
|
|
350
436
|
});
|
|
351
437
|
}
|
|
352
438
|
|
|
439
|
+
async function callAi(aiConfig, prompt, livePrinter, options = {}) {
|
|
440
|
+
const maxRetries = options.maxRetries || RETRY_CONFIG.maxRetries;
|
|
441
|
+
const retryDelay = options.retryDelay || RETRY_CONFIG.retryDelay;
|
|
442
|
+
const enableLogging = options.silent === false || RETRY_CONFIG.enableRetryLogging;
|
|
443
|
+
|
|
444
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
445
|
+
try {
|
|
446
|
+
const timestamp = new Date().toISOString();
|
|
447
|
+
if (enableLogging) {
|
|
448
|
+
console.log(`[${timestamp}] [AI Call - Attempt ${attempt}/${maxRetries}] Invoking ${aiConfig.label}...`);
|
|
449
|
+
}
|
|
450
|
+
if (attempt > 1) {
|
|
451
|
+
retryStats.totalRetries++;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const result = await callAiBase(aiConfig, prompt, livePrinter);
|
|
455
|
+
|
|
456
|
+
if (attempt > 1) {
|
|
457
|
+
retryStats.successfulRetries++;
|
|
458
|
+
if (enableLogging) {
|
|
459
|
+
console.log(`[AI Call - Success] Recovered after ${attempt} attempts`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return result;
|
|
463
|
+
} catch (error) {
|
|
464
|
+
const isLastAttempt = attempt === maxRetries;
|
|
465
|
+
const timestamp = new Date().toISOString();
|
|
466
|
+
|
|
467
|
+
if (enableLogging) {
|
|
468
|
+
console.error(`[${timestamp}] [AI Call - Attempt ${attempt}/${maxRetries}] Failed`);
|
|
469
|
+
console.error(` Provider: ${aiConfig.label}`);
|
|
470
|
+
console.error(` Error: ${error.message}`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (!isLastAttempt) {
|
|
474
|
+
if (enableLogging) {
|
|
475
|
+
console.log(`[AI Call - Retry] Waiting ${retryDelay/1000} seconds before retry...`);
|
|
476
|
+
}
|
|
477
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
478
|
+
} else {
|
|
479
|
+
if (enableLogging) {
|
|
480
|
+
console.error(`[AI Call - Failed] Max retries reached. AI call failed permanently.`);
|
|
481
|
+
}
|
|
482
|
+
retryStats.failedCommands.push({
|
|
483
|
+
command: `AI call to ${aiConfig.label}`,
|
|
484
|
+
error: error.message,
|
|
485
|
+
timestamp: timestamp
|
|
486
|
+
});
|
|
487
|
+
throw error;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
353
493
|
// ===== SESSION RECORDING =====
|
|
354
494
|
// Records full prompts, responses, git diffs, and timing for model training
|
|
355
495
|
|
|
@@ -359,8 +499,134 @@ function ensureSessionDir() {
|
|
|
359
499
|
if (!fs.existsSync(SESSIONS_DIR)) fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
360
500
|
}
|
|
361
501
|
|
|
362
|
-
|
|
363
|
-
|
|
502
|
+
// Async version with proper setTimeout
|
|
503
|
+
async function safeExecAsync(cmd, cwd, options = {}) {
|
|
504
|
+
const { exec } = require('child_process');
|
|
505
|
+
const util = require('util');
|
|
506
|
+
const execAsync = util.promisify(exec);
|
|
507
|
+
|
|
508
|
+
const maxRetries = options.maxRetries || RETRY_CONFIG.maxRetries;
|
|
509
|
+
const retryDelay = options.retryDelay || RETRY_CONFIG.retryDelay;
|
|
510
|
+
const timeout = options.timeout || RETRY_CONFIG.gitTimeout;
|
|
511
|
+
const silent = options.silent || !RETRY_CONFIG.enableRetryLogging;
|
|
512
|
+
|
|
513
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
514
|
+
try {
|
|
515
|
+
if (!silent && attempt > 1) {
|
|
516
|
+
console.log(`[Git Retry ${attempt}/${maxRetries}] Executing: ${cmd.substring(0, 50)}...`);
|
|
517
|
+
retryStats.totalRetries++;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const { stdout } = await execAsync(cmd, {
|
|
521
|
+
cwd,
|
|
522
|
+
encoding: 'utf8',
|
|
523
|
+
timeout,
|
|
524
|
+
maxBuffer: 10 * 1024 * 1024 // 10MB buffer
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
const result = stdout.trim();
|
|
528
|
+
|
|
529
|
+
if (attempt > 1) {
|
|
530
|
+
retryStats.successfulRetries++;
|
|
531
|
+
if (!silent) {
|
|
532
|
+
console.log(`[Git Success] Command recovered after ${attempt} attempts`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return result;
|
|
536
|
+
} catch (e) {
|
|
537
|
+
const isLastAttempt = attempt === maxRetries;
|
|
538
|
+
if (!silent) {
|
|
539
|
+
const timestamp = new Date().toISOString();
|
|
540
|
+
console.error(`[${timestamp}] [Git Attempt ${attempt}/${maxRetries}] Command failed: ${cmd.substring(0, 50)}...`);
|
|
541
|
+
console.error(` Error: ${e.message || 'Unknown error'}`);
|
|
542
|
+
if (e.code === 'ETIMEDOUT') {
|
|
543
|
+
console.error(` Timeout after ${timeout/1000} seconds`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (!isLastAttempt) {
|
|
548
|
+
if (!silent) {
|
|
549
|
+
console.log(` Waiting ${retryDelay/1000} seconds before retry...`);
|
|
550
|
+
}
|
|
551
|
+
// Proper async delay
|
|
552
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
553
|
+
} else {
|
|
554
|
+
if (!silent) {
|
|
555
|
+
console.error(` Max retries reached. Command failed permanently.`);
|
|
556
|
+
}
|
|
557
|
+
retryStats.failedCommands.push({
|
|
558
|
+
command: cmd.substring(0, 100),
|
|
559
|
+
error: e.message,
|
|
560
|
+
timestamp: new Date().toISOString()
|
|
561
|
+
});
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Sync version (kept for backward compatibility)
|
|
570
|
+
function safeExec(cmd, cwd, options = {}) {
|
|
571
|
+
const maxRetries = options.maxRetries || RETRY_CONFIG.maxRetries;
|
|
572
|
+
const retryDelay = options.retryDelay || RETRY_CONFIG.retryDelay;
|
|
573
|
+
const timeout = options.timeout || RETRY_CONFIG.gitTimeout;
|
|
574
|
+
const silent = options.silent || !RETRY_CONFIG.enableRetryLogging;
|
|
575
|
+
|
|
576
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
577
|
+
try {
|
|
578
|
+
if (!silent && attempt > 1) {
|
|
579
|
+
console.log(`[Git Retry ${attempt}/${maxRetries}] Executing: ${cmd.substring(0, 50)}...`);
|
|
580
|
+
retryStats.totalRetries++;
|
|
581
|
+
}
|
|
582
|
+
const result = execSync(cmd, { cwd, encoding: 'utf8', timeout }).trim();
|
|
583
|
+
if (attempt > 1) {
|
|
584
|
+
retryStats.successfulRetries++;
|
|
585
|
+
if (!silent) {
|
|
586
|
+
console.log(`[Git Success] Command recovered after ${attempt} attempts`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return result;
|
|
590
|
+
} catch (e) {
|
|
591
|
+
const isLastAttempt = attempt === maxRetries;
|
|
592
|
+
if (!silent) {
|
|
593
|
+
const timestamp = new Date().toISOString();
|
|
594
|
+
console.error(`[${timestamp}] [Git Attempt ${attempt}/${maxRetries}] Command failed: ${cmd.substring(0, 50)}...`);
|
|
595
|
+
console.error(` Error: ${e.message || 'Unknown error'}`);
|
|
596
|
+
if (e.code === 'ETIMEDOUT') {
|
|
597
|
+
console.error(` Timeout after ${timeout/1000} seconds`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (!isLastAttempt) {
|
|
602
|
+
if (!silent) {
|
|
603
|
+
console.log(` Waiting ${retryDelay/1000} seconds before retry...`);
|
|
604
|
+
}
|
|
605
|
+
// Cross-platform sleep
|
|
606
|
+
const sleepCmd = process.platform === 'win32'
|
|
607
|
+
? `powershell -Command "Start-Sleep -Seconds ${retryDelay/1000}"`
|
|
608
|
+
: `sleep ${retryDelay/1000}`;
|
|
609
|
+
try {
|
|
610
|
+
execSync(sleepCmd, { stdio: 'ignore' });
|
|
611
|
+
} catch (sleepError) {
|
|
612
|
+
// If sleep command fails, we have to skip the delay
|
|
613
|
+
// Busy-wait is too CPU intensive and blocks the event loop
|
|
614
|
+
console.warn(`[Warning] Unable to sleep between retries, continuing immediately`);
|
|
615
|
+
}
|
|
616
|
+
} else {
|
|
617
|
+
if (!silent) {
|
|
618
|
+
console.error(` Max retries reached. Command failed permanently.`);
|
|
619
|
+
}
|
|
620
|
+
retryStats.failedCommands.push({
|
|
621
|
+
command: cmd.substring(0, 100),
|
|
622
|
+
error: e.message,
|
|
623
|
+
timestamp: new Date().toISOString()
|
|
624
|
+
});
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return null;
|
|
364
630
|
}
|
|
365
631
|
|
|
366
632
|
function slugify(text) {
|
|
@@ -404,6 +670,34 @@ class SessionRecorder {
|
|
|
404
670
|
s.error = error || null;
|
|
405
671
|
}
|
|
406
672
|
|
|
673
|
+
async saveAsync() {
|
|
674
|
+
const record = {
|
|
675
|
+
sessionId: this.sessionId, version: '1.0.0',
|
|
676
|
+
task: this.task, workspace: this.workspace, aiProvider: this.aiProvider,
|
|
677
|
+
startedAt: this.startedAt, completedAt: new Date().toISOString(),
|
|
678
|
+
totalDurationMs: Date.now() - new Date(this.startedAt).getTime(),
|
|
679
|
+
steps: this.steps,
|
|
680
|
+
totalSteps: this.steps.length,
|
|
681
|
+
completedSteps: this.steps.filter(s => s.completedAt && !s.error).length,
|
|
682
|
+
git: {
|
|
683
|
+
initialCommit: this.initialCommit,
|
|
684
|
+
finalCommit: await safeExecAsync('git rev-parse --short HEAD', this.workspace) || '',
|
|
685
|
+
totalDiff: await safeExecAsync('git diff', this.workspace) || ''
|
|
686
|
+
},
|
|
687
|
+
environment: { platform: os.platform(), nodeVersion: process.version }
|
|
688
|
+
};
|
|
689
|
+
const filename = `${this.sessionId}-${slugify(this.task)}.json`;
|
|
690
|
+
const filepath = path.join(SESSIONS_DIR, filename);
|
|
691
|
+
await fs.promises.writeFile(filepath, JSON.stringify(record, null, 2), 'utf8');
|
|
692
|
+
// Append to index
|
|
693
|
+
try {
|
|
694
|
+
await fs.promises.appendFile(path.join(SESSIONS_DIR, 'index.jsonl'),
|
|
695
|
+
JSON.stringify({ sessionId: record.sessionId, task: record.task, startedAt: record.startedAt, totalDurationMs: record.totalDurationMs, file: filename }) + '\n');
|
|
696
|
+
} catch (e) {}
|
|
697
|
+
return filepath;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Sync version kept for backward compatibility
|
|
407
701
|
save() {
|
|
408
702
|
const record = {
|
|
409
703
|
sessionId: this.sessionId, version: '1.0.0',
|
|
@@ -484,7 +778,7 @@ async function run() {
|
|
|
484
778
|
globalContextInputs.push(timeline.sharedContext);
|
|
485
779
|
}
|
|
486
780
|
|
|
487
|
-
const globalContext = gatherContext(workspace, globalContextInputs);
|
|
781
|
+
const globalContext = await gatherContext(workspace, globalContextInputs);
|
|
488
782
|
const globalContextBlock = globalContext.sections.join('\n\n');
|
|
489
783
|
|
|
490
784
|
let scheduledSteps;
|
|
@@ -557,7 +851,7 @@ IMPORTANT: Work autonomously. Do not ask questions or wait for confirmation. Mak
|
|
|
557
851
|
Only work inside the provided workspace and preserve user data.`;
|
|
558
852
|
|
|
559
853
|
for (const step of scheduledSteps) {
|
|
560
|
-
const stepContext = gatherContext(workspace, step.contextPaths);
|
|
854
|
+
const stepContext = await gatherContext(workspace, step.contextPaths);
|
|
561
855
|
if (stepContext.labels.length) {
|
|
562
856
|
console.log(`Step context for ${step.displayName}: ${stepContext.labels.join(', ')}`);
|
|
563
857
|
console.log('');
|
|
@@ -614,10 +908,49 @@ Only work inside the provided workspace and preserve user data.`;
|
|
|
614
908
|
console.log('');
|
|
615
909
|
}
|
|
616
910
|
|
|
911
|
+
// Print retry statistics if any retries occurred
|
|
912
|
+
printRetryStatistics();
|
|
913
|
+
|
|
617
914
|
console.log('Session complete. Knoxis and the AI partner are standing by for further instructions.');
|
|
618
915
|
}
|
|
619
916
|
|
|
917
|
+
function printRetryStatistics() {
|
|
918
|
+
if (retryStats.totalRetries === 0 && retryStats.failedCommands.length === 0) {
|
|
919
|
+
return; // No retries needed, don't print stats
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
console.log('');
|
|
923
|
+
console.log('===== RETRY STATISTICS =====');
|
|
924
|
+
console.log(`Total Retries: ${retryStats.totalRetries}`);
|
|
925
|
+
console.log(`Successful Recoveries: ${retryStats.successfulRetries}`);
|
|
926
|
+
console.log(`Failed After Max Retries: ${retryStats.failedCommands.length}`);
|
|
927
|
+
|
|
928
|
+
if (retryStats.failedCommands.length > 0) {
|
|
929
|
+
console.log('\nFailed Commands:');
|
|
930
|
+
retryStats.failedCommands.forEach((failure, index) => {
|
|
931
|
+
console.log(` ${index + 1}. [${failure.timestamp}]`);
|
|
932
|
+
console.log(` Command: ${failure.command}`);
|
|
933
|
+
console.log(` Error: ${failure.error}`);
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const sessionDuration = Date.now() - retryStats.startTime;
|
|
938
|
+
const minutes = Math.floor(sessionDuration / 60000);
|
|
939
|
+
const seconds = Math.floor((sessionDuration % 60000) / 1000);
|
|
940
|
+
console.log(`\nSession Duration: ${minutes}m ${seconds}s`);
|
|
941
|
+
|
|
942
|
+
if (retryStats.totalRetries > 0) {
|
|
943
|
+
const retryOverhead = retryStats.totalRetries * RETRY_CONFIG.retryDelay;
|
|
944
|
+
const overheadMinutes = Math.floor(retryOverhead / 60000);
|
|
945
|
+
const overheadSeconds = Math.floor((retryOverhead % 60000) / 1000);
|
|
946
|
+
console.log(`Estimated Retry Overhead: ${overheadMinutes}m ${overheadSeconds}s`);
|
|
947
|
+
}
|
|
948
|
+
console.log('============================');
|
|
949
|
+
console.log('');
|
|
950
|
+
}
|
|
951
|
+
|
|
620
952
|
run().catch(err => {
|
|
621
|
-
console.error(err.message);
|
|
953
|
+
console.error('Fatal error:', err.message);
|
|
954
|
+
printRetryStatistics();
|
|
622
955
|
process.exit(1);
|
|
623
956
|
});
|