@flowdot.ai/daemon 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +45 -0
- package/README.md +51 -0
- package/dist/goals/DependencyResolver.d.ts +54 -0
- package/dist/goals/DependencyResolver.js +329 -0
- package/dist/goals/ErrorRecovery.d.ts +133 -0
- package/dist/goals/ErrorRecovery.js +489 -0
- package/dist/goals/GoalApiClient.d.ts +81 -0
- package/dist/goals/GoalApiClient.js +743 -0
- package/dist/goals/GoalCache.d.ts +65 -0
- package/dist/goals/GoalCache.js +243 -0
- package/dist/goals/GoalCommsHandler.d.ts +150 -0
- package/dist/goals/GoalCommsHandler.js +378 -0
- package/dist/goals/GoalExporter.d.ts +164 -0
- package/dist/goals/GoalExporter.js +318 -0
- package/dist/goals/GoalImporter.d.ts +107 -0
- package/dist/goals/GoalImporter.js +345 -0
- package/dist/goals/GoalManager.d.ts +110 -0
- package/dist/goals/GoalManager.js +535 -0
- package/dist/goals/GoalReporter.d.ts +105 -0
- package/dist/goals/GoalReporter.js +534 -0
- package/dist/goals/GoalScheduler.d.ts +102 -0
- package/dist/goals/GoalScheduler.js +209 -0
- package/dist/goals/GoalValidator.d.ts +72 -0
- package/dist/goals/GoalValidator.js +657 -0
- package/dist/goals/MetaGoalEnforcer.d.ts +111 -0
- package/dist/goals/MetaGoalEnforcer.js +536 -0
- package/dist/goals/MilestoneBreaker.d.ts +74 -0
- package/dist/goals/MilestoneBreaker.js +348 -0
- package/dist/goals/PermissionBridge.d.ts +109 -0
- package/dist/goals/PermissionBridge.js +326 -0
- package/dist/goals/ProgressTracker.d.ts +113 -0
- package/dist/goals/ProgressTracker.js +324 -0
- package/dist/goals/ReviewScheduler.d.ts +106 -0
- package/dist/goals/ReviewScheduler.js +360 -0
- package/dist/goals/TaskExecutor.d.ts +116 -0
- package/dist/goals/TaskExecutor.js +370 -0
- package/dist/goals/TaskFeedback.d.ts +126 -0
- package/dist/goals/TaskFeedback.js +402 -0
- package/dist/goals/TaskGenerator.d.ts +75 -0
- package/dist/goals/TaskGenerator.js +329 -0
- package/dist/goals/TaskQueue.d.ts +84 -0
- package/dist/goals/TaskQueue.js +331 -0
- package/dist/goals/TaskSanitizer.d.ts +61 -0
- package/dist/goals/TaskSanitizer.js +464 -0
- package/dist/goals/errors.d.ts +116 -0
- package/dist/goals/errors.js +299 -0
- package/dist/goals/index.d.ts +24 -0
- package/dist/goals/index.js +23 -0
- package/dist/goals/types.d.ts +395 -0
- package/dist/goals/types.js +230 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/loop/DaemonIPC.d.ts +67 -0
- package/dist/loop/DaemonIPC.js +358 -0
- package/dist/loop/IntervalParser.d.ts +39 -0
- package/dist/loop/IntervalParser.js +217 -0
- package/dist/loop/LoopDaemon.d.ts +123 -0
- package/dist/loop/LoopDaemon.js +1821 -0
- package/dist/loop/LoopExecutor.d.ts +93 -0
- package/dist/loop/LoopExecutor.js +326 -0
- package/dist/loop/LoopManager.d.ts +79 -0
- package/dist/loop/LoopManager.js +476 -0
- package/dist/loop/LoopScheduler.d.ts +69 -0
- package/dist/loop/LoopScheduler.js +329 -0
- package/dist/loop/LoopStore.d.ts +57 -0
- package/dist/loop/LoopStore.js +406 -0
- package/dist/loop/LoopValidator.d.ts +55 -0
- package/dist/loop/LoopValidator.js +603 -0
- package/dist/loop/errors.d.ts +115 -0
- package/dist/loop/errors.js +312 -0
- package/dist/loop/index.d.ts +11 -0
- package/dist/loop/index.js +10 -0
- package/dist/loop/notifications/Notifier.d.ts +28 -0
- package/dist/loop/notifications/Notifier.js +78 -0
- package/dist/loop/notifications/SlackNotifier.d.ts +28 -0
- package/dist/loop/notifications/SlackNotifier.js +203 -0
- package/dist/loop/notifications/TerminalNotifier.d.ts +18 -0
- package/dist/loop/notifications/TerminalNotifier.js +72 -0
- package/dist/loop/notifications/WebhookNotifier.d.ts +24 -0
- package/dist/loop/notifications/WebhookNotifier.js +123 -0
- package/dist/loop/notifications/index.d.ts +24 -0
- package/dist/loop/notifications/index.js +109 -0
- package/dist/loop/types.d.ts +280 -0
- package/dist/loop/types.js +222 -0
- package/package.json +92 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { GoalError } from './errors.js';
|
|
3
|
+
const noopLogger = {
|
|
4
|
+
debug: () => { },
|
|
5
|
+
info: () => { },
|
|
6
|
+
warn: () => { },
|
|
7
|
+
error: () => { },
|
|
8
|
+
};
|
|
9
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
10
|
+
maxAttempts: 3,
|
|
11
|
+
baseDelayMs: 1000,
|
|
12
|
+
multiplier: 2,
|
|
13
|
+
maxDelayMs: 30000,
|
|
14
|
+
jitter: 0.1,
|
|
15
|
+
};
|
|
16
|
+
const DEFAULT_MAX_CHECKPOINTS = 10;
|
|
17
|
+
const DEFAULT_ESCALATE_THRESHOLD = 3;
|
|
18
|
+
const TRANSIENT_ERROR_PATTERNS = [
|
|
19
|
+
/timeout/i,
|
|
20
|
+
/connection refused/i,
|
|
21
|
+
/network/i,
|
|
22
|
+
/temporary/i,
|
|
23
|
+
/retry/i,
|
|
24
|
+
/rate limit/i,
|
|
25
|
+
/429/,
|
|
26
|
+
/503/,
|
|
27
|
+
/ETIMEDOUT/,
|
|
28
|
+
/ECONNRESET/,
|
|
29
|
+
/ECONNREFUSED/,
|
|
30
|
+
];
|
|
31
|
+
const PERMISSION_ERROR_PATTERNS = [
|
|
32
|
+
/permission denied/i,
|
|
33
|
+
/access denied/i,
|
|
34
|
+
/unauthorized/i,
|
|
35
|
+
/forbidden/i,
|
|
36
|
+
/401/,
|
|
37
|
+
/403/,
|
|
38
|
+
];
|
|
39
|
+
const RESOURCE_ERROR_PATTERNS = [
|
|
40
|
+
/not found/i,
|
|
41
|
+
/does not exist/i,
|
|
42
|
+
/no such file/i,
|
|
43
|
+
/404/,
|
|
44
|
+
/ENOENT/,
|
|
45
|
+
];
|
|
46
|
+
export class ErrorRecoveryError extends GoalError {
|
|
47
|
+
constructor(message, cause) {
|
|
48
|
+
super('RECOVERY_ERROR', message, cause ? { cause } : {});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export class CheckpointNotFoundError extends GoalError {
|
|
52
|
+
checkpointId;
|
|
53
|
+
constructor(checkpointId) {
|
|
54
|
+
super('CHECKPOINT_NOT_FOUND', `Checkpoint not found: ${checkpointId}`);
|
|
55
|
+
this.checkpointId = checkpointId;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export class ErrorRecovery extends EventEmitter {
|
|
59
|
+
logger;
|
|
60
|
+
retryConfig;
|
|
61
|
+
maxCheckpointsPerGoal;
|
|
62
|
+
autoRetryTransient;
|
|
63
|
+
escalateThreshold;
|
|
64
|
+
checkpoints = new Map();
|
|
65
|
+
failureCounts = new Map();
|
|
66
|
+
retryState = new Map();
|
|
67
|
+
constructor(options = {}) {
|
|
68
|
+
super();
|
|
69
|
+
this.logger = options.logger ?? noopLogger;
|
|
70
|
+
this.retryConfig = { ...DEFAULT_RETRY_CONFIG, ...options.retryConfig };
|
|
71
|
+
this.maxCheckpointsPerGoal = options.maxCheckpointsPerGoal ?? DEFAULT_MAX_CHECKPOINTS;
|
|
72
|
+
this.autoRetryTransient = options.autoRetryTransient ?? true;
|
|
73
|
+
this.escalateThreshold = options.escalateThreshold ?? DEFAULT_ESCALATE_THRESHOLD;
|
|
74
|
+
}
|
|
75
|
+
createCheckpoint(goal, task, state, description) {
|
|
76
|
+
const checkpoint = {
|
|
77
|
+
id: this.generateCheckpointId(),
|
|
78
|
+
goalHash: goal.hash,
|
|
79
|
+
taskId: task.id,
|
|
80
|
+
taskStatus: task.status,
|
|
81
|
+
timestamp: new Date(),
|
|
82
|
+
stateData: state,
|
|
83
|
+
description,
|
|
84
|
+
};
|
|
85
|
+
let goalCheckpoints = this.checkpoints.get(goal.hash);
|
|
86
|
+
if (!goalCheckpoints) {
|
|
87
|
+
goalCheckpoints = [];
|
|
88
|
+
this.checkpoints.set(goal.hash, goalCheckpoints);
|
|
89
|
+
}
|
|
90
|
+
goalCheckpoints.push(checkpoint);
|
|
91
|
+
while (goalCheckpoints.length > this.maxCheckpointsPerGoal) {
|
|
92
|
+
goalCheckpoints.shift();
|
|
93
|
+
}
|
|
94
|
+
this.emit('checkpoint-created', checkpoint);
|
|
95
|
+
this.logger.debug('ERROR_RECOVERY', 'Checkpoint created', {
|
|
96
|
+
checkpointId: checkpoint.id,
|
|
97
|
+
goalHash: goal.hash,
|
|
98
|
+
taskId: task.id,
|
|
99
|
+
});
|
|
100
|
+
return checkpoint;
|
|
101
|
+
}
|
|
102
|
+
getCheckpoints(goalHash) {
|
|
103
|
+
return this.checkpoints.get(goalHash) ?? [];
|
|
104
|
+
}
|
|
105
|
+
getCheckpoint(checkpointId) {
|
|
106
|
+
for (const checkpoints of this.checkpoints.values()) {
|
|
107
|
+
const found = checkpoints.find((c) => c.id === checkpointId);
|
|
108
|
+
if (found)
|
|
109
|
+
return found;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
async restoreCheckpoint(checkpointId, restoreFunction) {
|
|
114
|
+
const checkpoint = this.getCheckpoint(checkpointId);
|
|
115
|
+
if (!checkpoint) {
|
|
116
|
+
throw new CheckpointNotFoundError(checkpointId);
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
await restoreFunction(checkpoint.stateData);
|
|
120
|
+
this.emit('checkpoint-restored', checkpoint);
|
|
121
|
+
this.logger.info('ERROR_RECOVERY', 'Checkpoint restored', {
|
|
122
|
+
checkpointId,
|
|
123
|
+
goalHash: checkpoint.goalHash,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
128
|
+
this.logger.error('ERROR_RECOVERY', 'Failed to restore checkpoint', {
|
|
129
|
+
checkpointId,
|
|
130
|
+
error: err.message,
|
|
131
|
+
});
|
|
132
|
+
throw new ErrorRecoveryError(`Failed to restore checkpoint: ${err.message}`, err);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
clearCheckpoints(goalHash) {
|
|
136
|
+
this.checkpoints.delete(goalHash);
|
|
137
|
+
this.logger.debug('ERROR_RECOVERY', 'Checkpoints cleared', { goalHash });
|
|
138
|
+
}
|
|
139
|
+
analyzeError(result) {
|
|
140
|
+
const error = result.error ?? 'Unknown error';
|
|
141
|
+
const category = this.categorizeError(error);
|
|
142
|
+
const isRecoverable = this.isRecoverable(category, result);
|
|
143
|
+
const recommendedStrategy = this.recommendStrategy(category, result);
|
|
144
|
+
const suggestions = this.generateSuggestions(category, error, result);
|
|
145
|
+
const confidence = this.calculateConfidence(category, error);
|
|
146
|
+
const analysis = {
|
|
147
|
+
category,
|
|
148
|
+
rootCause: this.extractRootCause(error),
|
|
149
|
+
isRecoverable,
|
|
150
|
+
recommendedStrategy,
|
|
151
|
+
confidence,
|
|
152
|
+
suggestions,
|
|
153
|
+
};
|
|
154
|
+
this.emit('error-analyzed', analysis, result);
|
|
155
|
+
this.logger.debug('ERROR_RECOVERY', 'Error analyzed', {
|
|
156
|
+
taskId: result.taskId,
|
|
157
|
+
category,
|
|
158
|
+
recommendedStrategy,
|
|
159
|
+
});
|
|
160
|
+
return analysis;
|
|
161
|
+
}
|
|
162
|
+
categorizeError(error) {
|
|
163
|
+
if (PERMISSION_ERROR_PATTERNS.some((p) => p.test(error))) {
|
|
164
|
+
return 'permission';
|
|
165
|
+
}
|
|
166
|
+
if (TRANSIENT_ERROR_PATTERNS.some((p) => p.test(error))) {
|
|
167
|
+
return 'transient';
|
|
168
|
+
}
|
|
169
|
+
if (RESOURCE_ERROR_PATTERNS.some((p) => p.test(error))) {
|
|
170
|
+
return 'resource';
|
|
171
|
+
}
|
|
172
|
+
if (/timeout/i.test(error)) {
|
|
173
|
+
return 'timeout';
|
|
174
|
+
}
|
|
175
|
+
if (/network|connection|socket/i.test(error)) {
|
|
176
|
+
return 'network';
|
|
177
|
+
}
|
|
178
|
+
if (/valid|invalid|format|parse/i.test(error)) {
|
|
179
|
+
return 'validation';
|
|
180
|
+
}
|
|
181
|
+
if (/system|internal|fatal/i.test(error)) {
|
|
182
|
+
return 'system';
|
|
183
|
+
}
|
|
184
|
+
return 'unknown';
|
|
185
|
+
}
|
|
186
|
+
isRecoverable(category, result) {
|
|
187
|
+
switch (category) {
|
|
188
|
+
case 'transient':
|
|
189
|
+
case 'timeout':
|
|
190
|
+
case 'network':
|
|
191
|
+
return true;
|
|
192
|
+
case 'validation':
|
|
193
|
+
return false;
|
|
194
|
+
case 'permission':
|
|
195
|
+
return false;
|
|
196
|
+
case 'resource':
|
|
197
|
+
return false;
|
|
198
|
+
case 'system':
|
|
199
|
+
return false;
|
|
200
|
+
case 'unknown': {
|
|
201
|
+
const retryState = this.retryState.get(result.taskId);
|
|
202
|
+
return !retryState || retryState.attempts < this.retryConfig.maxAttempts;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
recommendStrategy(category, result) {
|
|
207
|
+
const failureCount = this.failureCounts.get(result.taskId) ?? 0;
|
|
208
|
+
if (failureCount >= this.escalateThreshold) {
|
|
209
|
+
return 'escalate';
|
|
210
|
+
}
|
|
211
|
+
switch (category) {
|
|
212
|
+
case 'transient':
|
|
213
|
+
case 'timeout':
|
|
214
|
+
case 'network':
|
|
215
|
+
return 'retry';
|
|
216
|
+
case 'permission':
|
|
217
|
+
return 'escalate';
|
|
218
|
+
case 'validation':
|
|
219
|
+
return 'skip';
|
|
220
|
+
case 'resource':
|
|
221
|
+
return 'escalate';
|
|
222
|
+
case 'system':
|
|
223
|
+
return 'abort';
|
|
224
|
+
case 'unknown':
|
|
225
|
+
return 'retry';
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
generateSuggestions(category, error, _result) {
|
|
229
|
+
const suggestions = [];
|
|
230
|
+
switch (category) {
|
|
231
|
+
case 'transient':
|
|
232
|
+
suggestions.push('Wait a moment and retry the operation');
|
|
233
|
+
suggestions.push('Check if external services are available');
|
|
234
|
+
break;
|
|
235
|
+
case 'timeout':
|
|
236
|
+
suggestions.push('Increase timeout settings');
|
|
237
|
+
suggestions.push('Check network connectivity');
|
|
238
|
+
suggestions.push('Break the task into smaller operations');
|
|
239
|
+
break;
|
|
240
|
+
case 'network':
|
|
241
|
+
suggestions.push('Check network connectivity');
|
|
242
|
+
suggestions.push('Verify firewall settings');
|
|
243
|
+
suggestions.push('Check if remote service is available');
|
|
244
|
+
break;
|
|
245
|
+
case 'permission':
|
|
246
|
+
suggestions.push('Review and approve the required permission');
|
|
247
|
+
suggestions.push('Check goal restrictions configuration');
|
|
248
|
+
suggestions.push('Verify file/directory permissions');
|
|
249
|
+
break;
|
|
250
|
+
case 'validation':
|
|
251
|
+
suggestions.push('Review task parameters');
|
|
252
|
+
suggestions.push('Check input data format');
|
|
253
|
+
suggestions.push('Update task configuration');
|
|
254
|
+
break;
|
|
255
|
+
case 'resource':
|
|
256
|
+
suggestions.push('Verify the resource exists');
|
|
257
|
+
suggestions.push('Check file paths and URLs');
|
|
258
|
+
suggestions.push('Create missing resources if needed');
|
|
259
|
+
break;
|
|
260
|
+
case 'system':
|
|
261
|
+
suggestions.push('Check system logs for details');
|
|
262
|
+
suggestions.push('Restart the daemon if needed');
|
|
263
|
+
suggestions.push('Contact support if issue persists');
|
|
264
|
+
break;
|
|
265
|
+
default:
|
|
266
|
+
suggestions.push('Review the error details');
|
|
267
|
+
suggestions.push('Try the operation again');
|
|
268
|
+
}
|
|
269
|
+
if (/rate limit/i.test(error)) {
|
|
270
|
+
suggestions.push('Wait before retrying (rate limited)');
|
|
271
|
+
}
|
|
272
|
+
if (/memory/i.test(error)) {
|
|
273
|
+
suggestions.push('Free up system memory');
|
|
274
|
+
}
|
|
275
|
+
if (/disk/i.test(error)) {
|
|
276
|
+
suggestions.push('Check disk space availability');
|
|
277
|
+
}
|
|
278
|
+
return suggestions;
|
|
279
|
+
}
|
|
280
|
+
extractRootCause(error) {
|
|
281
|
+
const colonIndex = error.indexOf(':');
|
|
282
|
+
if (colonIndex > 0 && colonIndex < 50) {
|
|
283
|
+
return error.substring(0, colonIndex).trim();
|
|
284
|
+
}
|
|
285
|
+
if (error.length > 100) {
|
|
286
|
+
return error.substring(0, 100) + '...';
|
|
287
|
+
}
|
|
288
|
+
return error;
|
|
289
|
+
}
|
|
290
|
+
calculateConfidence(category, error) {
|
|
291
|
+
let confidence = 0.5;
|
|
292
|
+
const specificPatterns = [
|
|
293
|
+
PERMISSION_ERROR_PATTERNS,
|
|
294
|
+
TRANSIENT_ERROR_PATTERNS,
|
|
295
|
+
RESOURCE_ERROR_PATTERNS,
|
|
296
|
+
];
|
|
297
|
+
for (const patterns of specificPatterns) {
|
|
298
|
+
if (patterns.some((p) => p.test(error))) {
|
|
299
|
+
confidence += 0.2;
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (category === 'unknown') {
|
|
304
|
+
confidence -= 0.2;
|
|
305
|
+
}
|
|
306
|
+
if (/\b(400|401|403|404|429|500|502|503)\b/.test(error)) {
|
|
307
|
+
confidence += 0.1;
|
|
308
|
+
}
|
|
309
|
+
return Math.min(1, Math.max(0, confidence));
|
|
310
|
+
}
|
|
311
|
+
async executeRecovery(action, handlers) {
|
|
312
|
+
this.emit('recovery-started', action);
|
|
313
|
+
try {
|
|
314
|
+
if (action.delayMs > 0) {
|
|
315
|
+
await this.delay(action.delayMs);
|
|
316
|
+
}
|
|
317
|
+
let result;
|
|
318
|
+
switch (action.type) {
|
|
319
|
+
case 'retry': {
|
|
320
|
+
this.updateRetryState(action.taskId, action.attempt);
|
|
321
|
+
const execResult = await handlers.retry(action.taskId);
|
|
322
|
+
result = {
|
|
323
|
+
success: execResult.success,
|
|
324
|
+
strategy: 'retry',
|
|
325
|
+
details: execResult.success
|
|
326
|
+
? 'Retry succeeded'
|
|
327
|
+
: `Retry failed: ${execResult.error}`,
|
|
328
|
+
newStatus: execResult.status,
|
|
329
|
+
shouldContinue: execResult.success,
|
|
330
|
+
};
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
case 'skip': {
|
|
334
|
+
await handlers.skip(action.taskId);
|
|
335
|
+
result = {
|
|
336
|
+
success: true,
|
|
337
|
+
strategy: 'skip',
|
|
338
|
+
details: 'Task skipped',
|
|
339
|
+
newStatus: 'skipped',
|
|
340
|
+
shouldContinue: true,
|
|
341
|
+
};
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
case 'rollback': {
|
|
345
|
+
if (!action.checkpointId) {
|
|
346
|
+
throw new ErrorRecoveryError('Checkpoint ID required for rollback');
|
|
347
|
+
}
|
|
348
|
+
await handlers.rollback(action.checkpointId);
|
|
349
|
+
result = {
|
|
350
|
+
success: true,
|
|
351
|
+
strategy: 'rollback',
|
|
352
|
+
details: `Rolled back to checkpoint ${action.checkpointId}`,
|
|
353
|
+
shouldContinue: true,
|
|
354
|
+
};
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
case 'escalate': {
|
|
358
|
+
await handlers.escalate(action.taskId, action.goalHash);
|
|
359
|
+
this.emit('escalation-triggered', {
|
|
360
|
+
goalHash: action.goalHash,
|
|
361
|
+
taskId: action.taskId,
|
|
362
|
+
reason: 'Recovery escalation',
|
|
363
|
+
});
|
|
364
|
+
result = {
|
|
365
|
+
success: true,
|
|
366
|
+
strategy: 'escalate',
|
|
367
|
+
details: 'Escalated to user',
|
|
368
|
+
shouldContinue: false,
|
|
369
|
+
};
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
case 'abort': {
|
|
373
|
+
await handlers.abort(action.goalHash);
|
|
374
|
+
result = {
|
|
375
|
+
success: true,
|
|
376
|
+
strategy: 'abort',
|
|
377
|
+
details: 'Goal execution aborted',
|
|
378
|
+
shouldContinue: false,
|
|
379
|
+
};
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
default:
|
|
383
|
+
throw new ErrorRecoveryError(`Unknown recovery strategy: ${action.type}`);
|
|
384
|
+
}
|
|
385
|
+
this.emit('recovery-completed', result, action);
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
390
|
+
const result = {
|
|
391
|
+
success: false,
|
|
392
|
+
strategy: action.type,
|
|
393
|
+
details: `Recovery failed: ${err.message}`,
|
|
394
|
+
shouldContinue: false,
|
|
395
|
+
};
|
|
396
|
+
this.emit('recovery-failed', result, action);
|
|
397
|
+
this.emit('error', err);
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
createRecoveryAction(strategy, taskId, goalHash, options) {
|
|
402
|
+
const retryState = this.retryState.get(taskId);
|
|
403
|
+
const attempt = (retryState?.attempts ?? 0) + 1;
|
|
404
|
+
let delayMs = 0;
|
|
405
|
+
if (strategy === 'retry' && attempt > 1) {
|
|
406
|
+
delayMs = this.calculateRetryDelay(attempt);
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
type: strategy,
|
|
410
|
+
taskId,
|
|
411
|
+
goalHash,
|
|
412
|
+
attempt,
|
|
413
|
+
maxAttempts: this.retryConfig.maxAttempts,
|
|
414
|
+
delayMs: options?.customDelay ?? delayMs,
|
|
415
|
+
checkpointId: options?.checkpointId,
|
|
416
|
+
timestamp: new Date(),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
calculateRetryDelay(attempt) {
|
|
420
|
+
const { baseDelayMs, multiplier, maxDelayMs, jitter } = this.retryConfig;
|
|
421
|
+
let delay = baseDelayMs * Math.pow(multiplier, attempt - 1);
|
|
422
|
+
delay = Math.min(delay, maxDelayMs);
|
|
423
|
+
const jitterAmount = delay * jitter * (Math.random() * 2 - 1);
|
|
424
|
+
delay = delay + jitterAmount;
|
|
425
|
+
return Math.round(Math.max(0, delay));
|
|
426
|
+
}
|
|
427
|
+
updateRetryState(taskId, attempt) {
|
|
428
|
+
this.retryState.set(taskId, {
|
|
429
|
+
attempts: attempt,
|
|
430
|
+
lastAttempt: new Date(),
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
canRetry(taskId) {
|
|
434
|
+
const state = this.retryState.get(taskId);
|
|
435
|
+
if (!state)
|
|
436
|
+
return true;
|
|
437
|
+
return state.attempts < this.retryConfig.maxAttempts;
|
|
438
|
+
}
|
|
439
|
+
getRemainingRetries(taskId) {
|
|
440
|
+
const state = this.retryState.get(taskId);
|
|
441
|
+
if (!state)
|
|
442
|
+
return this.retryConfig.maxAttempts;
|
|
443
|
+
return Math.max(0, this.retryConfig.maxAttempts - state.attempts);
|
|
444
|
+
}
|
|
445
|
+
resetRetryState(taskId) {
|
|
446
|
+
this.retryState.delete(taskId);
|
|
447
|
+
this.failureCounts.delete(taskId);
|
|
448
|
+
}
|
|
449
|
+
recordFailure(taskId) {
|
|
450
|
+
const count = (this.failureCounts.get(taskId) ?? 0) + 1;
|
|
451
|
+
this.failureCounts.set(taskId, count);
|
|
452
|
+
return count;
|
|
453
|
+
}
|
|
454
|
+
getFailureCount(taskId) {
|
|
455
|
+
return this.failureCounts.get(taskId) ?? 0;
|
|
456
|
+
}
|
|
457
|
+
shouldEscalate(taskId) {
|
|
458
|
+
return this.getFailureCount(taskId) >= this.escalateThreshold;
|
|
459
|
+
}
|
|
460
|
+
generateCheckpointId() {
|
|
461
|
+
return `cp_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
462
|
+
}
|
|
463
|
+
delay(ms) {
|
|
464
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
465
|
+
}
|
|
466
|
+
clearAll() {
|
|
467
|
+
this.checkpoints.clear();
|
|
468
|
+
this.failureCounts.clear();
|
|
469
|
+
this.retryState.clear();
|
|
470
|
+
this.logger.debug('ERROR_RECOVERY', 'All state cleared');
|
|
471
|
+
}
|
|
472
|
+
getStats() {
|
|
473
|
+
const goalCheckpoints = new Map();
|
|
474
|
+
let totalCheckpoints = 0;
|
|
475
|
+
for (const [goalHash, checkpoints] of this.checkpoints) {
|
|
476
|
+
goalCheckpoints.set(goalHash, checkpoints.length);
|
|
477
|
+
totalCheckpoints += checkpoints.length;
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
totalCheckpoints,
|
|
481
|
+
goalCheckpoints,
|
|
482
|
+
failedTasks: this.failureCounts.size,
|
|
483
|
+
pendingRetries: this.retryState.size,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
export function createErrorRecovery(options = {}) {
|
|
488
|
+
return new ErrorRecovery(options);
|
|
489
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Goal, GoalHash, Milestone, Task, GoalMemory, MetaGoal, GoalActionLog, GoalProgress, CreateGoalInput, UpdateGoalInput, CreateMilestoneInput, UpdateMilestoneInput, CreateTaskInput, UpdateTaskInput, CreateMemoryInput, UpdateMemoryInput, CreateMetaGoalInput, UpdateMetaGoalInput, TaskFeedbackInput, TaskFailInput, Logger } from './types.js';
|
|
2
|
+
export type TokenProvider = () => Promise<string | null>;
|
|
3
|
+
export interface GoalApiClientOptions {
|
|
4
|
+
readonly baseUrl: string;
|
|
5
|
+
readonly tokenProvider: TokenProvider;
|
|
6
|
+
readonly timeout?: number;
|
|
7
|
+
readonly retries?: number;
|
|
8
|
+
readonly logger?: Logger;
|
|
9
|
+
}
|
|
10
|
+
export interface PaginatedResponse<T> {
|
|
11
|
+
readonly data: T[];
|
|
12
|
+
readonly meta?: {
|
|
13
|
+
readonly currentPage: number;
|
|
14
|
+
readonly lastPage: number;
|
|
15
|
+
readonly perPage: number;
|
|
16
|
+
readonly total: number;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export interface ApiListOptions {
|
|
20
|
+
readonly status?: string;
|
|
21
|
+
readonly page?: number;
|
|
22
|
+
readonly perPage?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare class GoalApiClient {
|
|
25
|
+
private readonly baseUrl;
|
|
26
|
+
private readonly tokenProvider;
|
|
27
|
+
private readonly timeout;
|
|
28
|
+
private readonly retries;
|
|
29
|
+
private readonly logger;
|
|
30
|
+
constructor(options: GoalApiClientOptions);
|
|
31
|
+
listGoals(options?: ApiListOptions): Promise<Goal[]>;
|
|
32
|
+
getGoal(hash: GoalHash): Promise<Goal>;
|
|
33
|
+
createGoal(input: CreateGoalInput): Promise<Goal>;
|
|
34
|
+
updateGoal(hash: GoalHash, input: UpdateGoalInput): Promise<Goal>;
|
|
35
|
+
deleteGoal(hash: GoalHash): Promise<void>;
|
|
36
|
+
pauseGoal(hash: GoalHash): Promise<Goal>;
|
|
37
|
+
resumeGoal(hash: GoalHash): Promise<Goal>;
|
|
38
|
+
completeGoal(hash: GoalHash): Promise<Goal>;
|
|
39
|
+
abandonGoal(hash: GoalHash): Promise<Goal>;
|
|
40
|
+
getGoalProgress(hash: GoalHash): Promise<GoalProgress>;
|
|
41
|
+
listMilestones(goalHash: GoalHash): Promise<Milestone[]>;
|
|
42
|
+
createMilestone(goalHash: GoalHash, input: CreateMilestoneInput): Promise<Milestone>;
|
|
43
|
+
getMilestone(goalHash: GoalHash, milestoneId: number): Promise<Milestone>;
|
|
44
|
+
updateMilestone(goalHash: GoalHash, milestoneId: number, input: UpdateMilestoneInput): Promise<Milestone>;
|
|
45
|
+
deleteMilestone(goalHash: GoalHash, milestoneId: number): Promise<void>;
|
|
46
|
+
completeMilestone(goalHash: GoalHash, milestoneId: number): Promise<Milestone>;
|
|
47
|
+
listTasks(goalHash: GoalHash, options?: ApiListOptions): Promise<Task[]>;
|
|
48
|
+
getPendingTasks(): Promise<Task[]>;
|
|
49
|
+
getTasksAwaitingApproval(): Promise<Task[]>;
|
|
50
|
+
createTask(goalHash: GoalHash, input: CreateTaskInput): Promise<Task>;
|
|
51
|
+
getTask(goalHash: GoalHash, taskId: number): Promise<Task>;
|
|
52
|
+
updateTask(goalHash: GoalHash, taskId: number, input: UpdateTaskInput): Promise<Task>;
|
|
53
|
+
deleteTask(goalHash: GoalHash, taskId: number): Promise<void>;
|
|
54
|
+
approveTask(goalHash: GoalHash, taskId: number): Promise<Task>;
|
|
55
|
+
denyTask(goalHash: GoalHash, taskId: number): Promise<Task>;
|
|
56
|
+
startTask(goalHash: GoalHash, taskId: number): Promise<Task>;
|
|
57
|
+
completeTask(goalHash: GoalHash, taskId: number, result?: Record<string, unknown>): Promise<Task>;
|
|
58
|
+
failTask(goalHash: GoalHash, taskId: number, input: TaskFailInput): Promise<Task>;
|
|
59
|
+
skipTask(goalHash: GoalHash, taskId: number): Promise<Task>;
|
|
60
|
+
addTaskFeedback(goalHash: GoalHash, taskId: number, input: TaskFeedbackInput): Promise<Task>;
|
|
61
|
+
listMemories(goalHash: GoalHash): Promise<GoalMemory[]>;
|
|
62
|
+
getEnabledMemories(goalHash: GoalHash): Promise<GoalMemory[]>;
|
|
63
|
+
createMemory(goalHash: GoalHash, input: CreateMemoryInput): Promise<GoalMemory>;
|
|
64
|
+
updateMemory(goalHash: GoalHash, memoryId: number, input: UpdateMemoryInput): Promise<GoalMemory>;
|
|
65
|
+
toggleMemory(goalHash: GoalHash, memoryId: number): Promise<GoalMemory>;
|
|
66
|
+
deleteMemory(goalHash: GoalHash, memoryId: number): Promise<void>;
|
|
67
|
+
listMetaGoals(active?: boolean): Promise<MetaGoal[]>;
|
|
68
|
+
getMetaGoalsForGoal(goalHash: GoalHash): Promise<MetaGoal[]>;
|
|
69
|
+
createMetaGoal(input: CreateMetaGoalInput): Promise<MetaGoal>;
|
|
70
|
+
getMetaGoal(id: number): Promise<MetaGoal>;
|
|
71
|
+
updateMetaGoal(id: number, input: UpdateMetaGoalInput): Promise<MetaGoal>;
|
|
72
|
+
toggleMetaGoal(id: number): Promise<MetaGoal>;
|
|
73
|
+
deleteMetaGoal(id: number): Promise<void>;
|
|
74
|
+
getActionLogs(goalHash: GoalHash, limit?: number): Promise<GoalActionLog[]>;
|
|
75
|
+
private transformGoalInput;
|
|
76
|
+
private request;
|
|
77
|
+
private executeRequest;
|
|
78
|
+
private handleHttpError;
|
|
79
|
+
private delay;
|
|
80
|
+
}
|
|
81
|
+
export declare function createGoalApiClient(options: GoalApiClientOptions): GoalApiClient;
|