vibecodingmachine-core 2025.12.25-25 → 2026.1.22-1441
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/ERROR_REPORTING_API.md +212 -0
- package/ERROR_REPORTING_USAGE.md +380 -0
- package/__tests__/provider-manager-fallback.test.js +43 -0
- package/__tests__/provider-manager-rate-limit.test.js +61 -0
- package/__tests__/utils/git-branch-manager.test.js +61 -0
- package/package.json +1 -1
- package/src/beta-request.js +160 -0
- package/src/compliance/compliance-manager.js +5 -2
- package/src/database/migrations.js +135 -12
- package/src/database/user-database-client.js +127 -8
- package/src/database/user-schema.js +28 -0
- package/src/health-tracking/__tests__/ide-health-tracker.test.js +420 -0
- package/src/health-tracking/__tests__/interaction-recorder.test.js +392 -0
- package/src/health-tracking/errors.js +50 -0
- package/src/health-tracking/health-reporter.js +331 -0
- package/src/health-tracking/ide-health-tracker.js +446 -0
- package/src/health-tracking/interaction-recorder.js +161 -0
- package/src/health-tracking/json-storage.js +276 -0
- package/src/health-tracking/storage-interface.js +63 -0
- package/src/health-tracking/validators.js +277 -0
- package/src/ide-integration/applescript-manager.cjs +1087 -9
- package/src/ide-integration/applescript-manager.js +565 -15
- package/src/ide-integration/applescript-utils.js +26 -18
- package/src/ide-integration/provider-manager.cjs +158 -28
- package/src/ide-integration/quota-detector.cjs +339 -16
- package/src/ide-integration/quota-detector.js +6 -1
- package/src/index.cjs +36 -1
- package/src/index.js +20 -0
- package/src/localization/translations/en.js +15 -1
- package/src/localization/translations/es.js +14 -0
- package/src/requirement-numbering.js +164 -0
- package/src/sync/aws-setup.js +4 -4
- package/src/utils/admin-utils.js +33 -0
- package/src/utils/error-reporter.js +117 -0
- package/src/utils/git-branch-manager.js +278 -0
- package/src/utils/requirement-helpers.js +44 -5
- package/src/utils/requirements-parser.js +28 -3
- package/tests/health-tracking/health-reporter.test.js +329 -0
- package/tests/health-tracking/ide-health-tracker.test.js +368 -0
- package/tests/health-tracking/interaction-recorder.test.js +309 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IDE Health Tracker
|
|
3
|
+
*
|
|
4
|
+
* Main class for tracking IDE reliability metrics.
|
|
5
|
+
* Tracks success/failure counters, response times, and EWMA calculations.
|
|
6
|
+
*
|
|
7
|
+
* @module ide-health-tracker
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const EventEmitter = require('events');
|
|
11
|
+
const { JSONStorage } = require('./json-storage');
|
|
12
|
+
const { InteractionRecorder } = require('./interaction-recorder');
|
|
13
|
+
const { ValidationError } = require('./errors');
|
|
14
|
+
const { validateIdeId, validateResponseTime } = require('./validators');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Maximum number of response times to keep per IDE
|
|
18
|
+
*/
|
|
19
|
+
const MAX_RESPONSE_TIMES = 50;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Consecutive failure threshold for emitting warning
|
|
23
|
+
*/
|
|
24
|
+
const CONSECUTIVE_FAILURE_THRESHOLD = 5;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Default EWMA alpha value
|
|
28
|
+
*/
|
|
29
|
+
const DEFAULT_EWMA_ALPHA = 0.3;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Tracks IDE health metrics including success/failure counts and response times
|
|
33
|
+
* @extends EventEmitter
|
|
34
|
+
*/
|
|
35
|
+
class IDEHealthTracker extends EventEmitter {
|
|
36
|
+
/**
|
|
37
|
+
* @param {Object} options - Configuration options
|
|
38
|
+
* @param {string} [options.storageFile] - Path to storage file
|
|
39
|
+
* @param {boolean} [options.autoSave=true] - Automatically save after updates
|
|
40
|
+
* @param {number} [options.debounceMs=500] - Debounce time for auto-save
|
|
41
|
+
*/
|
|
42
|
+
constructor(options = {}) {
|
|
43
|
+
super();
|
|
44
|
+
|
|
45
|
+
this.storage = new JSONStorage({
|
|
46
|
+
storageFile: options.storageFile,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
this.recorder = new InteractionRecorder(this.storage);
|
|
50
|
+
|
|
51
|
+
this.autoSave = options.autoSave !== undefined ? options.autoSave : true;
|
|
52
|
+
this.debounceMs = options.debounceMs || 500;
|
|
53
|
+
this.saveTimeout = null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Record a successful IDE interaction
|
|
58
|
+
* @param {string} ideId - IDE identifier
|
|
59
|
+
* @param {number} responseTime - Response time in milliseconds
|
|
60
|
+
* @param {Object} [metadata] - Additional metadata
|
|
61
|
+
* @param {number} [metadata.continuationPromptsDetected=0] - Number of prompts handled
|
|
62
|
+
* @param {string} [metadata.requirementId] - Associated requirement ID
|
|
63
|
+
* @param {Date} [metadata.timestamp] - Override timestamp
|
|
64
|
+
* @returns {Promise<void>}
|
|
65
|
+
* @throws {ValidationError} If validation fails
|
|
66
|
+
*/
|
|
67
|
+
async recordSuccess(ideId, responseTime, metadata = {}) {
|
|
68
|
+
// Validate inputs
|
|
69
|
+
validateIdeId(ideId);
|
|
70
|
+
validateResponseTime(responseTime);
|
|
71
|
+
|
|
72
|
+
// Read current data
|
|
73
|
+
const data = await this.storage.read();
|
|
74
|
+
|
|
75
|
+
// Initialize IDE record if needed
|
|
76
|
+
if (!data.ides[ideId]) {
|
|
77
|
+
data.ides[ideId] = this._createDefaultIDERecord();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const ideRecord = data.ides[ideId];
|
|
81
|
+
|
|
82
|
+
// Increment success count
|
|
83
|
+
ideRecord.successCount++;
|
|
84
|
+
|
|
85
|
+
// Reset consecutive failures
|
|
86
|
+
ideRecord.consecutiveFailures = 0;
|
|
87
|
+
|
|
88
|
+
// Update lastSuccess
|
|
89
|
+
ideRecord.lastSuccess = (metadata.timestamp || new Date()).toISOString();
|
|
90
|
+
|
|
91
|
+
// Add response time to array
|
|
92
|
+
ideRecord.responseTimes.push(responseTime);
|
|
93
|
+
|
|
94
|
+
// Enforce max response times
|
|
95
|
+
if (ideRecord.responseTimes.length > MAX_RESPONSE_TIMES) {
|
|
96
|
+
ideRecord.responseTimes = ideRecord.responseTimes.slice(-MAX_RESPONSE_TIMES);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Recalculate EWMA
|
|
100
|
+
ideRecord.ewma = this._calculateEWMA(ideRecord.responseTimes);
|
|
101
|
+
|
|
102
|
+
// Update current timeout based on EWMA and config
|
|
103
|
+
ideRecord.currentTimeout = await this._calculateTimeout(ideRecord, data.timeoutConfig);
|
|
104
|
+
|
|
105
|
+
// Write updated data
|
|
106
|
+
await this.storage.write(data);
|
|
107
|
+
|
|
108
|
+
// Record interaction
|
|
109
|
+
await this.recorder.record({
|
|
110
|
+
ideId,
|
|
111
|
+
timestamp: metadata.timestamp || new Date(),
|
|
112
|
+
outcome: 'success',
|
|
113
|
+
responseTime,
|
|
114
|
+
timeoutUsed: ideRecord.currentTimeout,
|
|
115
|
+
continuationPromptsDetected: metadata.continuationPromptsDetected || 0,
|
|
116
|
+
requirementId: metadata.requirementId || null,
|
|
117
|
+
errorMessage: null,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Emit event
|
|
121
|
+
this.emit('health-updated', { ideId, outcome: 'success' });
|
|
122
|
+
|
|
123
|
+
// Trigger debounced save if auto-save enabled
|
|
124
|
+
if (this.autoSave) {
|
|
125
|
+
this._debouncedSave();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Record a failed IDE interaction
|
|
131
|
+
* @param {string} ideId - IDE identifier
|
|
132
|
+
* @param {string} [errorMessage] - Error description
|
|
133
|
+
* @param {Object} [metadata] - Additional metadata
|
|
134
|
+
* @param {number} [metadata.timeoutUsed] - Timeout that expired
|
|
135
|
+
* @param {string} [metadata.requirementId] - Associated requirement ID
|
|
136
|
+
* @param {Date} [metadata.timestamp] - Override timestamp
|
|
137
|
+
* @returns {Promise<void>}
|
|
138
|
+
* @throws {ValidationError} If validation fails
|
|
139
|
+
*/
|
|
140
|
+
async recordFailure(ideId, errorMessage, metadata = {}) {
|
|
141
|
+
// Validate inputs
|
|
142
|
+
validateIdeId(ideId);
|
|
143
|
+
|
|
144
|
+
// Read current data
|
|
145
|
+
const data = await this.storage.read();
|
|
146
|
+
|
|
147
|
+
// Initialize IDE record if needed
|
|
148
|
+
if (!data.ides[ideId]) {
|
|
149
|
+
data.ides[ideId] = this._createDefaultIDERecord();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const ideRecord = data.ides[ideId];
|
|
153
|
+
|
|
154
|
+
// Increment failure count
|
|
155
|
+
ideRecord.failureCount++;
|
|
156
|
+
|
|
157
|
+
// Increment consecutive failures
|
|
158
|
+
ideRecord.consecutiveFailures++;
|
|
159
|
+
|
|
160
|
+
// Update lastFailure
|
|
161
|
+
ideRecord.lastFailure = (metadata.timestamp || new Date()).toISOString();
|
|
162
|
+
|
|
163
|
+
// Write updated data
|
|
164
|
+
await this.storage.write(data);
|
|
165
|
+
|
|
166
|
+
// Record interaction
|
|
167
|
+
await this.recorder.record({
|
|
168
|
+
ideId,
|
|
169
|
+
timestamp: metadata.timestamp || new Date(),
|
|
170
|
+
outcome: 'failure',
|
|
171
|
+
responseTime: null,
|
|
172
|
+
timeoutUsed: metadata.timeoutUsed || ideRecord.currentTimeout,
|
|
173
|
+
continuationPromptsDetected: 0,
|
|
174
|
+
requirementId: metadata.requirementId || null,
|
|
175
|
+
errorMessage: errorMessage || 'Unknown error',
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Emit event
|
|
179
|
+
this.emit('health-updated', { ideId, outcome: 'failure' });
|
|
180
|
+
|
|
181
|
+
// Emit consecutive-failures event if threshold reached
|
|
182
|
+
if (ideRecord.consecutiveFailures === CONSECUTIVE_FAILURE_THRESHOLD) {
|
|
183
|
+
this.emit('consecutive-failures', {
|
|
184
|
+
ideId,
|
|
185
|
+
count: ideRecord.consecutiveFailures,
|
|
186
|
+
lastError: errorMessage || 'Unknown error',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Trigger debounced save if auto-save enabled
|
|
191
|
+
if (this.autoSave) {
|
|
192
|
+
this._debouncedSave();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Record a quota limit event (does NOT increment success/failure counters)
|
|
198
|
+
* @param {string} ideId - IDE identifier
|
|
199
|
+
* @param {string} quotaMessage - Quota message from IDE
|
|
200
|
+
* @param {Object} [metadata] - Additional metadata
|
|
201
|
+
* @param {string} [metadata.requirementId] - Associated requirement ID
|
|
202
|
+
* @param {Date} [metadata.timestamp] - Override timestamp
|
|
203
|
+
* @returns {Promise<void>}
|
|
204
|
+
* @throws {ValidationError} If validation fails
|
|
205
|
+
*/
|
|
206
|
+
async recordQuota(ideId, quotaMessage, metadata = {}) {
|
|
207
|
+
// Validate inputs
|
|
208
|
+
validateIdeId(ideId);
|
|
209
|
+
|
|
210
|
+
// Read current data
|
|
211
|
+
const data = await this.storage.read();
|
|
212
|
+
|
|
213
|
+
// Initialize IDE record if needed
|
|
214
|
+
if (!data.ides[ideId]) {
|
|
215
|
+
data.ides[ideId] = this._createDefaultIDERecord();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const ideRecord = data.ides[ideId];
|
|
219
|
+
|
|
220
|
+
// Record interaction (no counter updates per FR-008)
|
|
221
|
+
await this.recorder.record({
|
|
222
|
+
ideId,
|
|
223
|
+
timestamp: metadata.timestamp || new Date(),
|
|
224
|
+
outcome: 'quota',
|
|
225
|
+
responseTime: null,
|
|
226
|
+
timeoutUsed: ideRecord.currentTimeout,
|
|
227
|
+
continuationPromptsDetected: 0,
|
|
228
|
+
requirementId: metadata.requirementId || null,
|
|
229
|
+
errorMessage: quotaMessage,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Emit event
|
|
233
|
+
this.emit('health-updated', { ideId, outcome: 'quota' });
|
|
234
|
+
|
|
235
|
+
// Trigger debounced save if auto-save enabled
|
|
236
|
+
if (this.autoSave) {
|
|
237
|
+
this._debouncedSave();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get health metrics for a specific IDE
|
|
243
|
+
* @param {string} ideId - IDE identifier
|
|
244
|
+
* @returns {Promise<Object>} Health metrics object
|
|
245
|
+
*/
|
|
246
|
+
async getHealthMetrics(ideId) {
|
|
247
|
+
const data = await this.storage.read();
|
|
248
|
+
const ideRecord = data.ides[ideId];
|
|
249
|
+
|
|
250
|
+
if (!ideRecord) {
|
|
251
|
+
// Return default metrics for unknown IDE
|
|
252
|
+
return {
|
|
253
|
+
successCount: 0,
|
|
254
|
+
failureCount: 0,
|
|
255
|
+
successRate: 0,
|
|
256
|
+
averageResponseTime: 0,
|
|
257
|
+
currentTimeout: 1800000,
|
|
258
|
+
consecutiveFailures: 0,
|
|
259
|
+
lastSuccess: null,
|
|
260
|
+
lastFailure: null,
|
|
261
|
+
totalInteractions: 0,
|
|
262
|
+
recentInteractions: [],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const totalInteractions = ideRecord.successCount + ideRecord.failureCount;
|
|
267
|
+
const successRate = totalInteractions > 0
|
|
268
|
+
? ideRecord.successCount / totalInteractions
|
|
269
|
+
: 0;
|
|
270
|
+
|
|
271
|
+
// Get recent 10 interactions
|
|
272
|
+
const recentInteractions = await this.recorder.getInteractions(ideId, 10);
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
successCount: ideRecord.successCount,
|
|
276
|
+
failureCount: ideRecord.failureCount,
|
|
277
|
+
successRate,
|
|
278
|
+
averageResponseTime: ideRecord.ewma,
|
|
279
|
+
currentTimeout: ideRecord.currentTimeout,
|
|
280
|
+
consecutiveFailures: ideRecord.consecutiveFailures,
|
|
281
|
+
lastSuccess: ideRecord.lastSuccess,
|
|
282
|
+
lastFailure: ideRecord.lastFailure,
|
|
283
|
+
totalInteractions,
|
|
284
|
+
recentInteractions,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Get health metrics for all tracked IDEs
|
|
290
|
+
* @returns {Promise<Map<string, Object>>} Map of IDE ID to health metrics
|
|
291
|
+
*/
|
|
292
|
+
async getAllHealthMetrics() {
|
|
293
|
+
const data = await this.storage.read();
|
|
294
|
+
const metricsMap = new Map();
|
|
295
|
+
|
|
296
|
+
for (const ideId of Object.keys(data.ides)) {
|
|
297
|
+
const metrics = await this.getHealthMetrics(ideId);
|
|
298
|
+
metricsMap.set(ideId, metrics);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return metricsMap;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Get recommended IDE based on health metrics
|
|
306
|
+
* @param {Object} [options] - Options
|
|
307
|
+
* @param {number} [options.minInteractions=10] - Minimum total interactions to consider
|
|
308
|
+
* @param {boolean} [options.weightRecent=true] - Weight recent interactions more heavily
|
|
309
|
+
* @returns {Promise<string|null>} IDE identifier with highest success rate, or null
|
|
310
|
+
*/
|
|
311
|
+
async getRecommendedIDE(options = {}) {
|
|
312
|
+
const minInteractions = options.minInteractions || 10;
|
|
313
|
+
const allMetrics = await this.getAllHealthMetrics();
|
|
314
|
+
|
|
315
|
+
let bestIDE = null;
|
|
316
|
+
let bestSuccessRate = 0;
|
|
317
|
+
|
|
318
|
+
for (const [ideId, metrics] of allMetrics) {
|
|
319
|
+
if (metrics.totalInteractions >= minInteractions) {
|
|
320
|
+
if (metrics.successRate > bestSuccessRate) {
|
|
321
|
+
bestSuccessRate = metrics.successRate;
|
|
322
|
+
bestIDE = ideId;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return bestIDE;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Manually trigger save operation
|
|
332
|
+
* @returns {Promise<void>}
|
|
333
|
+
* @throws {FileSystemError} If write fails
|
|
334
|
+
*/
|
|
335
|
+
async save() {
|
|
336
|
+
// Cancel any pending debounced save
|
|
337
|
+
if (this.saveTimeout) {
|
|
338
|
+
clearTimeout(this.saveTimeout);
|
|
339
|
+
this.saveTimeout = null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Note: Data is already written in record methods,
|
|
343
|
+
// this is mainly for explicit save requests
|
|
344
|
+
const data = await this.storage.read();
|
|
345
|
+
await this.storage.write(data);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Reload health data from storage file
|
|
350
|
+
* @returns {Promise<void>}
|
|
351
|
+
* @throws {FileSystemError} If read fails
|
|
352
|
+
* @throws {ValidationError} If data is corrupted
|
|
353
|
+
*/
|
|
354
|
+
async load() {
|
|
355
|
+
// Simply reading from storage will load and validate the data
|
|
356
|
+
await this.storage.read();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Create default IDE record
|
|
361
|
+
* @private
|
|
362
|
+
* @returns {Object} Default IDE record
|
|
363
|
+
*/
|
|
364
|
+
_createDefaultIDERecord() {
|
|
365
|
+
return {
|
|
366
|
+
successCount: 0,
|
|
367
|
+
failureCount: 0,
|
|
368
|
+
responseTimes: [],
|
|
369
|
+
ewma: 0,
|
|
370
|
+
lastSuccess: null,
|
|
371
|
+
lastFailure: null,
|
|
372
|
+
consecutiveFailures: 0,
|
|
373
|
+
currentTimeout: 1800000, // 30 minutes
|
|
374
|
+
interactions: [],
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Calculate EWMA from response times
|
|
380
|
+
* @private
|
|
381
|
+
* @param {number[]} responseTimes - Array of response times
|
|
382
|
+
* @param {number} [alpha] - EWMA alpha parameter
|
|
383
|
+
* @returns {number} EWMA value
|
|
384
|
+
*/
|
|
385
|
+
_calculateEWMA(responseTimes, alpha = DEFAULT_EWMA_ALPHA) {
|
|
386
|
+
if (!responseTimes || responseTimes.length === 0) {
|
|
387
|
+
return 0;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
let ewma = responseTimes[0];
|
|
391
|
+
for (let i = 1; i < responseTimes.length; i++) {
|
|
392
|
+
ewma = alpha * responseTimes[i] + (1 - alpha) * ewma;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return ewma;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Calculate timeout for IDE based on EWMA and config
|
|
400
|
+
* @private
|
|
401
|
+
* @param {Object} ideRecord - IDE health record
|
|
402
|
+
* @param {Object} timeoutConfig - Timeout configuration
|
|
403
|
+
* @returns {Promise<number>} Calculated timeout in milliseconds
|
|
404
|
+
*/
|
|
405
|
+
async _calculateTimeout(ideRecord, timeoutConfig) {
|
|
406
|
+
if (!timeoutConfig || timeoutConfig.mode === 'fixed') {
|
|
407
|
+
return timeoutConfig?.defaultTimeout || 1800000;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Adaptive mode
|
|
411
|
+
const minSamples = timeoutConfig.minSamplesForAdaptive || 10;
|
|
412
|
+
if (ideRecord.successCount < minSamples) {
|
|
413
|
+
return timeoutConfig.defaultTimeout;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const baseTimeout = ideRecord.ewma;
|
|
417
|
+
const buffer = baseTimeout * (timeoutConfig.bufferPercentage || 0.4);
|
|
418
|
+
const adaptiveTimeout = baseTimeout + buffer;
|
|
419
|
+
|
|
420
|
+
// Enforce 5-minute minimum
|
|
421
|
+
return Math.max(adaptiveTimeout, 300000);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Debounced save operation
|
|
426
|
+
* @private
|
|
427
|
+
*/
|
|
428
|
+
_debouncedSave() {
|
|
429
|
+
if (this.saveTimeout) {
|
|
430
|
+
clearTimeout(this.saveTimeout);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
this.saveTimeout = setTimeout(async () => {
|
|
434
|
+
try {
|
|
435
|
+
await this.save();
|
|
436
|
+
} catch (error) {
|
|
437
|
+
this.emit('save-error', {
|
|
438
|
+
error,
|
|
439
|
+
retryCount: 0,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}, this.debounceMs);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
module.exports = { IDEHealthTracker, MAX_RESPONSE_TIMES, CONSECUTIVE_FAILURE_THRESHOLD };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interaction Recorder
|
|
3
|
+
*
|
|
4
|
+
* Records individual IDE interactions with timestamps and outcomes.
|
|
5
|
+
*
|
|
6
|
+
* @module interaction-recorder
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { ValidationError } = require('./errors');
|
|
10
|
+
const { validateIdeId, validateInteractionRecord } = require('./validators');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Maximum number of interactions to keep per IDE
|
|
14
|
+
*/
|
|
15
|
+
const MAX_INTERACTIONS = 100;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Default IDE health record structure
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_IDE_RECORD = {
|
|
21
|
+
successCount: 0,
|
|
22
|
+
failureCount: 0,
|
|
23
|
+
responseTimes: [],
|
|
24
|
+
ewma: 0,
|
|
25
|
+
lastSuccess: null,
|
|
26
|
+
lastFailure: null,
|
|
27
|
+
consecutiveFailures: 0,
|
|
28
|
+
currentTimeout: 1800000, // 30 minutes default
|
|
29
|
+
interactions: [],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Records individual interactions for IDE health tracking
|
|
34
|
+
*/
|
|
35
|
+
class InteractionRecorder {
|
|
36
|
+
/**
|
|
37
|
+
* @param {StorageInterface} storage - Storage interface with read() and write() methods
|
|
38
|
+
*/
|
|
39
|
+
constructor(storage) {
|
|
40
|
+
if (!storage || typeof storage.read !== 'function' || typeof storage.write !== 'function') {
|
|
41
|
+
throw new ValidationError(
|
|
42
|
+
'Storage must implement read() and write() methods',
|
|
43
|
+
'storage',
|
|
44
|
+
storage
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
this.storage = storage;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Record a single interaction
|
|
52
|
+
* @param {Object} interaction - Interaction object
|
|
53
|
+
* @param {string} interaction.ideId - IDE identifier
|
|
54
|
+
* @param {Date} interaction.timestamp - Interaction timestamp
|
|
55
|
+
* @param {string} interaction.outcome - 'success' | 'failure' | 'quota'
|
|
56
|
+
* @param {number|null} interaction.responseTime - Response time in ms (null for failures/quota)
|
|
57
|
+
* @param {number} interaction.timeoutUsed - Timeout value used
|
|
58
|
+
* @param {number} interaction.continuationPromptsDetected - Number of prompts detected
|
|
59
|
+
* @param {string|null} interaction.requirementId - Associated requirement ID
|
|
60
|
+
* @param {string|null} interaction.errorMessage - Error message (for failures)
|
|
61
|
+
* @returns {Promise<void>}
|
|
62
|
+
* @throws {ValidationError} If validation fails
|
|
63
|
+
*/
|
|
64
|
+
async record(interaction) {
|
|
65
|
+
// Validate ideId
|
|
66
|
+
validateIdeId(interaction.ideId);
|
|
67
|
+
|
|
68
|
+
// Convert Date to ISO string
|
|
69
|
+
const timestamp = interaction.timestamp instanceof Date
|
|
70
|
+
? interaction.timestamp.toISOString()
|
|
71
|
+
: interaction.timestamp;
|
|
72
|
+
|
|
73
|
+
// Create interaction record
|
|
74
|
+
const record = {
|
|
75
|
+
timestamp,
|
|
76
|
+
outcome: interaction.outcome,
|
|
77
|
+
responseTime: interaction.responseTime,
|
|
78
|
+
timeoutUsed: interaction.timeoutUsed,
|
|
79
|
+
continuationPromptsDetected: interaction.continuationPromptsDetected || 0,
|
|
80
|
+
requirementId: interaction.requirementId || null,
|
|
81
|
+
errorMessage: interaction.errorMessage || null,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Validate interaction record
|
|
85
|
+
validateInteractionRecord(record, interaction.ideId);
|
|
86
|
+
|
|
87
|
+
// Read current data
|
|
88
|
+
const data = await this.storage.read();
|
|
89
|
+
|
|
90
|
+
// Initialize IDE record if it doesn't exist
|
|
91
|
+
if (!data.ides[interaction.ideId]) {
|
|
92
|
+
data.ides[interaction.ideId] = JSON.parse(JSON.stringify(DEFAULT_IDE_RECORD));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Add interaction to array
|
|
96
|
+
data.ides[interaction.ideId].interactions.push(record);
|
|
97
|
+
|
|
98
|
+
// Enforce max interactions limit
|
|
99
|
+
if (data.ides[interaction.ideId].interactions.length > MAX_INTERACTIONS) {
|
|
100
|
+
// Remove oldest interactions
|
|
101
|
+
data.ides[interaction.ideId].interactions = data.ides[interaction.ideId].interactions.slice(-MAX_INTERACTIONS);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Write updated data
|
|
105
|
+
await this.storage.write(data);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get interactions for a specific IDE
|
|
110
|
+
* @param {string} ideId - IDE identifier
|
|
111
|
+
* @param {number} [limit] - Maximum number of interactions to return (most recent)
|
|
112
|
+
* @returns {Promise<Array>} Array of interaction records
|
|
113
|
+
*/
|
|
114
|
+
async getInteractions(ideId, limit) {
|
|
115
|
+
const data = await this.storage.read();
|
|
116
|
+
|
|
117
|
+
if (!data.ides[ideId] || !data.ides[ideId].interactions) {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const interactions = data.ides[ideId].interactions;
|
|
122
|
+
|
|
123
|
+
if (limit && limit > 0) {
|
|
124
|
+
return interactions.slice(-limit);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return interactions;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Clear all interactions for a specific IDE
|
|
132
|
+
* @param {string} ideId - IDE identifier
|
|
133
|
+
* @returns {Promise<void>}
|
|
134
|
+
*/
|
|
135
|
+
async clearInteractions(ideId) {
|
|
136
|
+
const data = await this.storage.read();
|
|
137
|
+
|
|
138
|
+
if (data.ides[ideId]) {
|
|
139
|
+
data.ides[ideId].interactions = [];
|
|
140
|
+
await this.storage.write(data);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get default IDE record structure
|
|
146
|
+
* @returns {Object} Default IDE record
|
|
147
|
+
*/
|
|
148
|
+
static getDefaultRecord() {
|
|
149
|
+
return JSON.parse(JSON.stringify(DEFAULT_IDE_RECORD));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get maximum interactions limit
|
|
154
|
+
* @returns {number} Max interactions per IDE
|
|
155
|
+
*/
|
|
156
|
+
static getMaxInteractions() {
|
|
157
|
+
return MAX_INTERACTIONS;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = { InteractionRecorder, MAX_INTERACTIONS, DEFAULT_IDE_RECORD };
|