ralphblaster-agent 0.1.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/LICENSE +21 -0
- package/README.md +294 -0
- package/bin/agent-dashboard.sh +168 -0
- package/bin/monitor-agent.sh +264 -0
- package/bin/ralphblaster.js +247 -0
- package/package.json +64 -0
- package/postinstall-colored.js +66 -0
- package/src/api-client.js +764 -0
- package/src/claude-plugin/.claude-plugin/plugin.json +9 -0
- package/src/claude-plugin/README.md +42 -0
- package/src/claude-plugin/skills/ralph/SKILL.md +259 -0
- package/src/commands/add-project.js +257 -0
- package/src/commands/init.js +79 -0
- package/src/config-file-manager.js +84 -0
- package/src/config.js +66 -0
- package/src/error-window.js +86 -0
- package/src/executor/claude-runner.js +716 -0
- package/src/executor/error-handler.js +65 -0
- package/src/executor/git-helper.js +196 -0
- package/src/executor/index.js +296 -0
- package/src/executor/job-handlers/clarifying-questions.js +213 -0
- package/src/executor/job-handlers/code-execution.js +145 -0
- package/src/executor/job-handlers/prd-generation.js +259 -0
- package/src/executor/path-helper.js +74 -0
- package/src/executor/prompt-validator.js +51 -0
- package/src/executor.js +4 -0
- package/src/index.js +342 -0
- package/src/logger.js +193 -0
- package/src/logging/README.md +93 -0
- package/src/logging/config.js +179 -0
- package/src/logging/destinations/README.md +290 -0
- package/src/logging/destinations/api-destination-unbatched.js +118 -0
- package/src/logging/destinations/api-destination.js +40 -0
- package/src/logging/destinations/base-destination.js +85 -0
- package/src/logging/destinations/batched-destination.js +198 -0
- package/src/logging/destinations/console-destination.js +172 -0
- package/src/logging/destinations/file-destination.js +208 -0
- package/src/logging/destinations/index.js +29 -0
- package/src/logging/destinations/progress-batch-destination-unbatched.js +92 -0
- package/src/logging/destinations/progress-batch-destination.js +41 -0
- package/src/logging/formatter.js +288 -0
- package/src/logging/log-manager.js +426 -0
- package/src/progress-throttle.js +101 -0
- package/src/system-monitor.js +64 -0
- package/src/utils/format.js +16 -0
- package/src/utils/log-file-helper.js +265 -0
- package/src/utils/progress-parser.js +250 -0
- package/src/worktree-manager.js +255 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LogManager - Coordinates multiple log destinations
|
|
3
|
+
*
|
|
4
|
+
* Acts as the main logging coordinator that routes log calls to all registered
|
|
5
|
+
* destinations (console, file, API, etc.) in parallel. Manages destination
|
|
6
|
+
* lifecycle and handles per-destination errors gracefully.
|
|
7
|
+
*
|
|
8
|
+
* Key features:
|
|
9
|
+
* - Dependency injection - destinations passed in, not created internally
|
|
10
|
+
* - Destination-agnostic - doesn't know about specific destination types
|
|
11
|
+
* - Parallel writes - logs to all destinations concurrently
|
|
12
|
+
* - Graceful error handling - destination failures don't affect other destinations
|
|
13
|
+
* - Context management - setJobContext/clearJobContext configures all destinations
|
|
14
|
+
* - Lifecycle management - setup, flush, and teardown coordination
|
|
15
|
+
*
|
|
16
|
+
* Example usage:
|
|
17
|
+
* ```javascript
|
|
18
|
+
* const { ConsoleDestination, ApiDestination } = require('./destinations');
|
|
19
|
+
* const loggingConfig = require('./config');
|
|
20
|
+
*
|
|
21
|
+
* const manager = new LogManager([
|
|
22
|
+
* new ConsoleDestination({ colors: loggingConfig.consoleColors }),
|
|
23
|
+
* new ApiDestination({ apiClient, jobId })
|
|
24
|
+
* ]);
|
|
25
|
+
*
|
|
26
|
+
* await manager.info('Server started', { port: 3000 });
|
|
27
|
+
* await manager.close();
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
class LogManager {
|
|
32
|
+
/**
|
|
33
|
+
* Create a new LogManager
|
|
34
|
+
* @param {Array<BaseDestination>} destinations - Array of log destinations to coordinate
|
|
35
|
+
* @param {Object} [config={}] - Manager configuration
|
|
36
|
+
* @param {string} [config.agentId=null] - Agent ID for multi-agent support
|
|
37
|
+
*/
|
|
38
|
+
constructor(destinations = [], config = {}) {
|
|
39
|
+
if (!Array.isArray(destinations)) {
|
|
40
|
+
throw new Error('LogManager requires an array of destinations');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.destinations = destinations;
|
|
44
|
+
this.agentId = config.agentId || null;
|
|
45
|
+
this.jobContext = {
|
|
46
|
+
jobId: null,
|
|
47
|
+
globalContext: {}
|
|
48
|
+
};
|
|
49
|
+
this.isShuttingDown = false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Write a log entry to all destinations
|
|
54
|
+
* Routes the log to all destinations in parallel, handling errors gracefully.
|
|
55
|
+
* Failed destinations don't affect other destinations.
|
|
56
|
+
* @param {string} level - Log level ('error', 'warn', 'info', 'debug')
|
|
57
|
+
* @param {string} message - Log message
|
|
58
|
+
* @param {Object} [data={}] - Structured metadata
|
|
59
|
+
* @returns {Promise<void>}
|
|
60
|
+
* @private
|
|
61
|
+
*/
|
|
62
|
+
async write(level, message, data = {}) {
|
|
63
|
+
// Merge global context with log data
|
|
64
|
+
const enrichedData = { ...this.jobContext.globalContext, ...data };
|
|
65
|
+
|
|
66
|
+
// Add agent ID to metadata if set
|
|
67
|
+
if (this.agentId) {
|
|
68
|
+
enrichedData.agentId = this.agentId;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Add job ID to metadata if set
|
|
72
|
+
if (this.jobContext.jobId) {
|
|
73
|
+
enrichedData.jobId = this.jobContext.jobId;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Write to all destinations in parallel
|
|
77
|
+
const writePromises = this.destinations.map(destination => {
|
|
78
|
+
// Check if destination should handle this log level
|
|
79
|
+
if (destination.shouldLog && !destination.shouldLog(level)) {
|
|
80
|
+
return Promise.resolve();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Write to destination, catching errors to prevent cascading failures
|
|
84
|
+
return destination.write(level, message, enrichedData)
|
|
85
|
+
.catch(error => {
|
|
86
|
+
// Let destination handle its own errors
|
|
87
|
+
if (typeof destination.handleError === 'function') {
|
|
88
|
+
destination.handleError(error, level, message);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Wait for all writes to complete (or fail gracefully)
|
|
94
|
+
await Promise.all(writePromises);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Log an error message
|
|
99
|
+
* Logs to all destinations that accept error level.
|
|
100
|
+
* @param {string} message - Error message
|
|
101
|
+
* @param {Object} [data={}] - Optional structured metadata
|
|
102
|
+
* @returns {Promise<void>}
|
|
103
|
+
* @example
|
|
104
|
+
* await manager.error('Database connection failed', { error: err.message })
|
|
105
|
+
*/
|
|
106
|
+
async error(message, data = {}) {
|
|
107
|
+
await this.write('error', message, data);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Log a warning message
|
|
112
|
+
* Logs to all destinations that accept warn level.
|
|
113
|
+
* @param {string} message - Warning message
|
|
114
|
+
* @param {Object} [data={}] - Optional structured metadata
|
|
115
|
+
* @returns {Promise<void>}
|
|
116
|
+
* @example
|
|
117
|
+
* await manager.warn('Disk space low', { available: '5%' })
|
|
118
|
+
*/
|
|
119
|
+
async warn(message, data = {}) {
|
|
120
|
+
await this.write('warn', message, data);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Log an info message
|
|
125
|
+
* Logs to all destinations that accept info level.
|
|
126
|
+
* @param {string} message - Info message
|
|
127
|
+
* @param {Object} [data={}] - Optional structured metadata
|
|
128
|
+
* @returns {Promise<void>}
|
|
129
|
+
* @example
|
|
130
|
+
* await manager.info('Server started', { port: 3000 })
|
|
131
|
+
*/
|
|
132
|
+
async info(message, data = {}) {
|
|
133
|
+
await this.write('info', message, data);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Log a debug message
|
|
138
|
+
* Logs to all destinations that accept debug level.
|
|
139
|
+
* @param {string} message - Debug message
|
|
140
|
+
* @param {Object} [data={}] - Optional structured metadata
|
|
141
|
+
* @returns {Promise<void>}
|
|
142
|
+
* @example
|
|
143
|
+
* await manager.debug('Request received', { headers, body })
|
|
144
|
+
*/
|
|
145
|
+
async debug(message, data = {}) {
|
|
146
|
+
await this.write('debug', message, data);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Set agent ID for multi-agent support
|
|
151
|
+
* Adds agent ID to all subsequent logs across all destinations.
|
|
152
|
+
* @param {string} id - Agent ID (e.g., 'agent-1', 'agent-2')
|
|
153
|
+
*/
|
|
154
|
+
setAgentId(id) {
|
|
155
|
+
this.agentId = id;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Set job context for logging
|
|
160
|
+
* Configures all destinations with job context. The job ID will be
|
|
161
|
+
* included in all subsequent logs.
|
|
162
|
+
* @param {number} jobId - Job ID to associate with logs
|
|
163
|
+
* @param {Object} [context={}] - Optional global context to add to all logs
|
|
164
|
+
* @example
|
|
165
|
+
* manager.setJobContext(123, { component: 'worktree' })
|
|
166
|
+
*/
|
|
167
|
+
setJobContext(jobId, context = {}) {
|
|
168
|
+
this.jobContext.jobId = jobId;
|
|
169
|
+
this.jobContext.globalContext = context;
|
|
170
|
+
|
|
171
|
+
// Notify destinations of context change (if they support it)
|
|
172
|
+
this.destinations.forEach(destination => {
|
|
173
|
+
if (typeof destination.setJobContext === 'function') {
|
|
174
|
+
destination.setJobContext(jobId, context);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Clear job context
|
|
181
|
+
* Clears the job context and flushes all destinations to ensure
|
|
182
|
+
* no logs are lost. Call this when a job completes.
|
|
183
|
+
* @returns {Promise<void>}
|
|
184
|
+
*/
|
|
185
|
+
async clearJobContext() {
|
|
186
|
+
// Flush all destinations before clearing context
|
|
187
|
+
await this.flush();
|
|
188
|
+
|
|
189
|
+
this.jobContext.jobId = null;
|
|
190
|
+
this.jobContext.globalContext = {};
|
|
191
|
+
|
|
192
|
+
// Notify destinations of context clear (if they support it)
|
|
193
|
+
const clearPromises = this.destinations.map(destination => {
|
|
194
|
+
if (typeof destination.clearJobContext === 'function') {
|
|
195
|
+
return destination.clearJobContext().catch(() => {
|
|
196
|
+
// Silently handle errors during context clear
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return Promise.resolve();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await Promise.all(clearPromises);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Set global context that will be included in all subsequent logs
|
|
207
|
+
* @param {string|Object} keyOrContext - Key name or object with multiple keys
|
|
208
|
+
* @param {*} [value] - Value (only if first param is a string)
|
|
209
|
+
* @example
|
|
210
|
+
* manager.setContext('component', 'worktree')
|
|
211
|
+
* manager.setContext({ component: 'worktree', operation: 'create' })
|
|
212
|
+
*/
|
|
213
|
+
setContext(keyOrContext, value) {
|
|
214
|
+
if (typeof keyOrContext === 'object') {
|
|
215
|
+
this.jobContext.globalContext = {
|
|
216
|
+
...this.jobContext.globalContext,
|
|
217
|
+
...keyOrContext
|
|
218
|
+
};
|
|
219
|
+
} else {
|
|
220
|
+
this.jobContext.globalContext[keyOrContext] = value;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Create a child logger with additional context
|
|
226
|
+
* Context is additive - child inherits parent context and adds its own.
|
|
227
|
+
* @param {Object} context - Additional context for this child logger
|
|
228
|
+
* @returns {Object} Child logger with all parent methods
|
|
229
|
+
* @example
|
|
230
|
+
* const worktreeLogger = manager.child({ component: 'worktree' })
|
|
231
|
+
* await worktreeLogger.info('Creating worktree') // Includes component: 'worktree'
|
|
232
|
+
*/
|
|
233
|
+
child(context) {
|
|
234
|
+
const childContext = { ...this.jobContext.globalContext, ...context };
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
error: (msg, data) => this.write('error', msg, { ...childContext, ...data }),
|
|
238
|
+
warn: (msg, data) => this.write('warn', msg, { ...childContext, ...data }),
|
|
239
|
+
info: (msg, data) => this.write('info', msg, { ...childContext, ...data }),
|
|
240
|
+
debug: (msg, data) => this.write('debug', msg, { ...childContext, ...data }),
|
|
241
|
+
event: (eventType, data) => this.event(eventType, { ...childContext, ...data }),
|
|
242
|
+
startTimer: (operation) => this.startTimer(operation, childContext),
|
|
243
|
+
measure: (operation, fn) => this.measure(operation, fn, childContext),
|
|
244
|
+
child: (additionalContext) => this.child({ ...childContext, ...additionalContext })
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Log a semantic event with structured metadata
|
|
250
|
+
* Automatically determines log level based on action (failed/error = error, else info).
|
|
251
|
+
* @param {string} eventType - Event type in format 'category.action' (e.g., 'worktree.created')
|
|
252
|
+
* @param {Object} [data={}] - Additional event data
|
|
253
|
+
* @returns {Promise<void>}
|
|
254
|
+
* @example
|
|
255
|
+
* await manager.event('worktree.created', { path: '/path/to/worktree', duration: 3200 })
|
|
256
|
+
*/
|
|
257
|
+
async event(eventType, data = {}) {
|
|
258
|
+
const [category, action] = eventType.split('.');
|
|
259
|
+
const eventData = {
|
|
260
|
+
eventType,
|
|
261
|
+
category,
|
|
262
|
+
action,
|
|
263
|
+
...data
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Determine level based on action
|
|
267
|
+
const level = action === 'failed' || action === 'error' ? 'error' : 'info';
|
|
268
|
+
|
|
269
|
+
// Format message from action
|
|
270
|
+
const message = action ? action.charAt(0).toUpperCase() + action.slice(1) : eventType;
|
|
271
|
+
|
|
272
|
+
await this.write(level, message, eventData);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Start a performance timer for an operation
|
|
277
|
+
* Returns a timer object with a done() method to log completion with duration.
|
|
278
|
+
* @param {string} operation - Operation name (e.g., 'worktree.create')
|
|
279
|
+
* @param {Object} [initialContext={}] - Initial context for the operation
|
|
280
|
+
* @returns {Object} Timer object with done() method
|
|
281
|
+
* @example
|
|
282
|
+
* const timer = manager.startTimer('worktree.create')
|
|
283
|
+
* // ... do work ...
|
|
284
|
+
* timer.done({ path: '/path/to/worktree' }) // Logs duration automatically
|
|
285
|
+
*/
|
|
286
|
+
startTimer(operation, initialContext = {}) {
|
|
287
|
+
const startTime = Date.now();
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
done: async (data = {}) => {
|
|
291
|
+
const duration = Date.now() - startTime;
|
|
292
|
+
await this.event(`${operation}.complete`, {
|
|
293
|
+
...initialContext,
|
|
294
|
+
...data,
|
|
295
|
+
duration,
|
|
296
|
+
durationMs: duration
|
|
297
|
+
});
|
|
298
|
+
return duration;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Measure and log an async operation automatically
|
|
305
|
+
* Logs operation start, measures duration, and logs completion with success status.
|
|
306
|
+
* @param {string} operation - Operation name (e.g., 'prd.conversion')
|
|
307
|
+
* @param {Function} fn - Async function to measure
|
|
308
|
+
* @param {Object} [context={}] - Additional context
|
|
309
|
+
* @returns {Promise<*>} Result of the async function
|
|
310
|
+
* @example
|
|
311
|
+
* const result = await manager.measure('prd.conversion', async () => {
|
|
312
|
+
* return await convertPRD(job)
|
|
313
|
+
* })
|
|
314
|
+
* // Logs: prd.conversion.started, then prd.conversion.complete with duration
|
|
315
|
+
*/
|
|
316
|
+
async measure(operation, fn, context = {}) {
|
|
317
|
+
await this.event(`${operation}.started`, context);
|
|
318
|
+
const timer = this.startTimer(operation, context);
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
const result = await fn();
|
|
322
|
+
await timer.done({ success: true });
|
|
323
|
+
return result;
|
|
324
|
+
} catch (error) {
|
|
325
|
+
await timer.done({ success: false, error: error.message });
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Flush all buffered logs across all destinations
|
|
332
|
+
* Ensures all pending logs are written immediately.
|
|
333
|
+
* Safe to call multiple times.
|
|
334
|
+
* @returns {Promise<void>}
|
|
335
|
+
*/
|
|
336
|
+
async flush() {
|
|
337
|
+
const flushPromises = this.destinations.map(destination => {
|
|
338
|
+
if (typeof destination.flush === 'function') {
|
|
339
|
+
return destination.flush().catch(error => {
|
|
340
|
+
// Let destination handle its own flush errors
|
|
341
|
+
if (typeof destination.handleError === 'function') {
|
|
342
|
+
destination.handleError(error, 'error', 'Failed to flush destination');
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return Promise.resolve();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
await Promise.all(flushPromises);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Close all destinations and release resources
|
|
354
|
+
* Flushes all pending logs, stops timers, closes connections, etc.
|
|
355
|
+
* Call this during shutdown to ensure no logs are lost.
|
|
356
|
+
* @returns {Promise<void>}
|
|
357
|
+
*/
|
|
358
|
+
async close() {
|
|
359
|
+
this.isShuttingDown = true;
|
|
360
|
+
|
|
361
|
+
const closePromises = this.destinations.map(destination => {
|
|
362
|
+
if (typeof destination.close === 'function') {
|
|
363
|
+
return destination.close().catch(error => {
|
|
364
|
+
// Let destination handle its own close errors
|
|
365
|
+
if (typeof destination.handleError === 'function') {
|
|
366
|
+
destination.handleError(error, 'error', 'Failed to close destination');
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return Promise.resolve();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
await Promise.all(closePromises);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Add a destination to the manager
|
|
378
|
+
* Useful for dynamically adding destinations after initialization.
|
|
379
|
+
* @param {BaseDestination} destination - Destination to add
|
|
380
|
+
*/
|
|
381
|
+
addDestination(destination) {
|
|
382
|
+
if (!destination) {
|
|
383
|
+
throw new Error('Cannot add null or undefined destination');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
this.destinations.push(destination);
|
|
387
|
+
|
|
388
|
+
// If we have active context, notify the new destination
|
|
389
|
+
if (this.jobContext.jobId && typeof destination.setJobContext === 'function') {
|
|
390
|
+
destination.setJobContext(this.jobContext.jobId, this.jobContext.globalContext);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Remove a destination from the manager
|
|
396
|
+
* Flushes and closes the destination before removing it.
|
|
397
|
+
* @param {BaseDestination} destination - Destination to remove
|
|
398
|
+
* @returns {Promise<void>}
|
|
399
|
+
*/
|
|
400
|
+
async removeDestination(destination) {
|
|
401
|
+
const index = this.destinations.indexOf(destination);
|
|
402
|
+
if (index === -1) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Close the destination before removing
|
|
407
|
+
if (typeof destination.close === 'function') {
|
|
408
|
+
await destination.close().catch(() => {
|
|
409
|
+
// Silently handle close errors during removal
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
this.destinations.splice(index, 1);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Get count of registered destinations
|
|
418
|
+
* Useful for testing and debugging.
|
|
419
|
+
* @returns {number} Number of destinations
|
|
420
|
+
*/
|
|
421
|
+
getDestinationCount() {
|
|
422
|
+
return this.destinations.length;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
module.exports = LogManager;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProgressThrottle - Adaptive throttling for progress updates
|
|
3
|
+
*
|
|
4
|
+
* Adjusts throttle interval based on update rate:
|
|
5
|
+
* - Low rate (<5/sec): 100ms throttle (responsive)
|
|
6
|
+
* - Medium rate (5-20/sec): 200ms throttle (balanced)
|
|
7
|
+
* - High rate (>20/sec): 500ms throttle (reduced load)
|
|
8
|
+
*
|
|
9
|
+
* This prevents API overload during high-output jobs while maintaining
|
|
10
|
+
* responsiveness for low-output jobs.
|
|
11
|
+
*/
|
|
12
|
+
class ProgressThrottle {
|
|
13
|
+
constructor() {
|
|
14
|
+
// Sliding window for rate calculation (5 seconds)
|
|
15
|
+
this.WINDOW_MS = 5000;
|
|
16
|
+
|
|
17
|
+
// Throttle intervals based on rate
|
|
18
|
+
this.INTERVALS = {
|
|
19
|
+
MIN: 100, // Low rate: minimal throttling
|
|
20
|
+
MEDIUM: 200, // Medium rate: moderate throttling
|
|
21
|
+
MAX: 500 // High rate: aggressive throttling
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Rate thresholds (updates per second)
|
|
25
|
+
this.RATE_THRESHOLDS = {
|
|
26
|
+
LOW: 5, // Below 5 updates/sec = low rate
|
|
27
|
+
MEDIUM: 20 // Above 20 updates/sec = high rate
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Tracking state
|
|
31
|
+
this.recentUpdates = []; // Timestamps of recent updates
|
|
32
|
+
this.lastUpdate = 0; // Timestamp of last update
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get current throttle interval based on recent update rate
|
|
37
|
+
* @returns {number} Throttle interval in milliseconds
|
|
38
|
+
*/
|
|
39
|
+
getInterval() {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
|
|
42
|
+
// Clean up old updates (beyond 5-second window)
|
|
43
|
+
this.recentUpdates = this.recentUpdates.filter(
|
|
44
|
+
timestamp => now - timestamp < this.WINDOW_MS
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// If no recent updates, use minimum interval
|
|
48
|
+
if (this.recentUpdates.length === 0) {
|
|
49
|
+
return this.INTERVALS.MIN;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Calculate actual time span of recent updates
|
|
53
|
+
const oldestUpdate = Math.min(...this.recentUpdates);
|
|
54
|
+
const timeSpanMs = now - oldestUpdate;
|
|
55
|
+
|
|
56
|
+
// If all updates happened very recently (< 1 second), use that time span
|
|
57
|
+
// Otherwise use the full window for rate calculation
|
|
58
|
+
const effectiveWindowMs = Math.max(timeSpanMs, 1000); // At least 1 second
|
|
59
|
+
const effectiveWindowSeconds = effectiveWindowMs / 1000;
|
|
60
|
+
|
|
61
|
+
// Calculate updates per second based on effective window
|
|
62
|
+
const updatesPerSecond = this.recentUpdates.length / effectiveWindowSeconds;
|
|
63
|
+
|
|
64
|
+
// Determine interval based on rate
|
|
65
|
+
if (updatesPerSecond > this.RATE_THRESHOLDS.MEDIUM) {
|
|
66
|
+
return this.INTERVALS.MAX; // High rate: aggressive throttling
|
|
67
|
+
} else if (updatesPerSecond > this.RATE_THRESHOLDS.LOW) {
|
|
68
|
+
return this.INTERVALS.MEDIUM; // Medium rate: moderate throttling
|
|
69
|
+
} else {
|
|
70
|
+
return this.INTERVALS.MIN; // Low rate: minimal throttling
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if update should be throttled
|
|
76
|
+
* @returns {boolean} True if should throttle (skip), false if should allow
|
|
77
|
+
*/
|
|
78
|
+
shouldThrottle() {
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
const interval = this.getInterval();
|
|
81
|
+
const timeSinceLastUpdate = now - this.lastUpdate;
|
|
82
|
+
|
|
83
|
+
return timeSinceLastUpdate < interval;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Record an update (call this when sending an update)
|
|
88
|
+
*/
|
|
89
|
+
recordUpdate() {
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
this.recentUpdates.push(now);
|
|
92
|
+
this.lastUpdate = now;
|
|
93
|
+
|
|
94
|
+
// Cleanup old updates to prevent memory growth
|
|
95
|
+
this.recentUpdates = this.recentUpdates.filter(
|
|
96
|
+
timestamp => now - timestamp < this.WINDOW_MS
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = ProgressThrottle;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SystemMonitor - Monitors system resources to determine agent capacity
|
|
5
|
+
*
|
|
6
|
+
* Adjusts agent capacity based on available memory and CPU load:
|
|
7
|
+
* - High resources (>40% mem, <0.6 CPU): capacity = 5
|
|
8
|
+
* - Medium resources (>20% mem, <0.8 CPU): capacity = 2
|
|
9
|
+
* - Low resources (<20% mem OR >0.8 CPU): capacity = 1
|
|
10
|
+
*
|
|
11
|
+
* This prevents agents from overwhelming systems that are already under load.
|
|
12
|
+
*/
|
|
13
|
+
class SystemMonitor {
|
|
14
|
+
constructor() {
|
|
15
|
+
// Thresholds for capacity determination
|
|
16
|
+
this.THRESHOLDS = {
|
|
17
|
+
// Memory thresholds (as fraction of total memory)
|
|
18
|
+
MEM_LOW: 0.2, // Below 20% free = low resources
|
|
19
|
+
MEM_MEDIUM: 0.4, // Below 40% free = medium resources
|
|
20
|
+
|
|
21
|
+
// CPU load thresholds (as fraction per CPU)
|
|
22
|
+
CPU_MEDIUM: 0.6, // Above 0.6 per CPU = medium load
|
|
23
|
+
CPU_HIGH: 0.8, // Above 0.8 per CPU = high load
|
|
24
|
+
|
|
25
|
+
// Capacity levels
|
|
26
|
+
CAPACITY_MIN: 1,
|
|
27
|
+
CAPACITY_MEDIUM: 2,
|
|
28
|
+
CAPACITY_MAX: 5
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get current system capacity based on resources
|
|
34
|
+
* @returns {number} Capacity level (1, 2, or 5)
|
|
35
|
+
*/
|
|
36
|
+
getCapacity() {
|
|
37
|
+
const freeMem = os.freemem();
|
|
38
|
+
const totalMem = os.totalmem();
|
|
39
|
+
const cpus = os.cpus();
|
|
40
|
+
const cpuCount = (cpus && cpus.length) || 1; // Default to 1 to avoid division by zero
|
|
41
|
+
const loadAvgArray = os.loadavg();
|
|
42
|
+
const loadAvg = (loadAvgArray && loadAvgArray[0]) || 0; // 1-minute load average
|
|
43
|
+
|
|
44
|
+
// Calculate resource metrics
|
|
45
|
+
const memFraction = freeMem / totalMem;
|
|
46
|
+
const cpuLoadPerCore = loadAvg / cpuCount;
|
|
47
|
+
|
|
48
|
+
// Determine capacity based on thresholds
|
|
49
|
+
// Low resources: return minimum capacity
|
|
50
|
+
if (memFraction < this.THRESHOLDS.MEM_LOW || cpuLoadPerCore > this.THRESHOLDS.CPU_HIGH) {
|
|
51
|
+
return this.THRESHOLDS.CAPACITY_MIN;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Medium resources: return medium capacity
|
|
55
|
+
if (memFraction < this.THRESHOLDS.MEM_MEDIUM || cpuLoadPerCore > this.THRESHOLDS.CPU_MEDIUM) {
|
|
56
|
+
return this.THRESHOLDS.CAPACITY_MEDIUM;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// High resources: return maximum capacity
|
|
60
|
+
return this.THRESHOLDS.CAPACITY_MAX;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = SystemMonitor;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format duration in human-readable form
|
|
3
|
+
* @param {number} ms - Duration in milliseconds
|
|
4
|
+
* @returns {string} Formatted duration
|
|
5
|
+
*/
|
|
6
|
+
function formatDuration(ms) {
|
|
7
|
+
if (ms < 1000) return `${ms}ms`;
|
|
8
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
9
|
+
const minutes = Math.floor(ms / 60000);
|
|
10
|
+
const seconds = Math.floor((ms % 60000) / 1000);
|
|
11
|
+
return `${minutes}m ${seconds}s`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
formatDuration
|
|
16
|
+
};
|