mark-improving-agent 2.2.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.
Files changed (79) hide show
  1. package/README.md +335 -0
  2. package/VERSION +1 -0
  3. package/bin/cli.js +12 -0
  4. package/dist/agent/context.js +78 -0
  5. package/dist/agent/index.js +6 -0
  6. package/dist/agent/runtime.js +195 -0
  7. package/dist/agent/task-graph.js +209 -0
  8. package/dist/agent/types.js +1 -0
  9. package/dist/cli/index.js +206 -0
  10. package/dist/core/cognition/active-inference.js +296 -0
  11. package/dist/core/cognition/cognitive-architecture.js +263 -0
  12. package/dist/core/cognition/dual-process.js +102 -0
  13. package/dist/core/cognition/index.js +13 -0
  14. package/dist/core/cognition/learning-from-failure.js +184 -0
  15. package/dist/core/cognition/meta-agent.js +407 -0
  16. package/dist/core/cognition/metacognition.js +322 -0
  17. package/dist/core/cognition/react.js +177 -0
  18. package/dist/core/cognition/retrieval-anchor.js +99 -0
  19. package/dist/core/cognition/self-evolution.js +294 -0
  20. package/dist/core/cognition/self-verification.js +190 -0
  21. package/dist/core/cognition/thought-graph.js +495 -0
  22. package/dist/core/cognition/tool-augmented-llm.js +188 -0
  23. package/dist/core/cognition/tool-execution-verifier.js +204 -0
  24. package/dist/core/collaboration/agentic-loop.js +165 -0
  25. package/dist/core/collaboration/index.js +3 -0
  26. package/dist/core/collaboration/multi-agent-system.js +186 -0
  27. package/dist/core/collaboration/multi-agent.js +110 -0
  28. package/dist/core/consciousness/emotion-engine.js +101 -0
  29. package/dist/core/consciousness/flow-machine.js +121 -0
  30. package/dist/core/consciousness/index.js +4 -0
  31. package/dist/core/consciousness/personality.js +103 -0
  32. package/dist/core/consciousness/types.js +1 -0
  33. package/dist/core/emotional-protocol.js +54 -0
  34. package/dist/core/evolution/engine.js +194 -0
  35. package/dist/core/evolution/goal-engine.js +153 -0
  36. package/dist/core/evolution/index.js +6 -0
  37. package/dist/core/evolution/meta-learning.js +172 -0
  38. package/dist/core/evolution/reflection.js +158 -0
  39. package/dist/core/evolution/self-healer.js +139 -0
  40. package/dist/core/evolution/types.js +1 -0
  41. package/dist/core/healing-rl.js +266 -0
  42. package/dist/core/heartbeat.js +408 -0
  43. package/dist/core/identity/index.js +3 -0
  44. package/dist/core/identity/reflexion.js +165 -0
  45. package/dist/core/identity/self-model.js +274 -0
  46. package/dist/core/identity/self-verifier.js +158 -0
  47. package/dist/core/identity/types.js +12 -0
  48. package/dist/core/lesson-bank.js +301 -0
  49. package/dist/core/memory/adaptive-rag.js +440 -0
  50. package/dist/core/memory/archive-store.js +187 -0
  51. package/dist/core/memory/dream-consolidation.js +366 -0
  52. package/dist/core/memory/embedder.js +130 -0
  53. package/dist/core/memory/hopfield-network.js +128 -0
  54. package/dist/core/memory/index.js +9 -0
  55. package/dist/core/memory/knowledge-graph.js +151 -0
  56. package/dist/core/memory/spaced-repetition.js +113 -0
  57. package/dist/core/memory/store.js +404 -0
  58. package/dist/core/memory/types.js +1 -0
  59. package/dist/core/psychology/analysis.js +456 -0
  60. package/dist/core/psychology/index.js +1 -0
  61. package/dist/core/rollback-manager.js +191 -0
  62. package/dist/core/security/index.js +1 -0
  63. package/dist/core/security/privacy.js +132 -0
  64. package/dist/core/truth-teller.js +253 -0
  65. package/dist/core/truthfulness.js +99 -0
  66. package/dist/core/types.js +2 -0
  67. package/dist/event/bus.js +47 -0
  68. package/dist/index.js +8 -0
  69. package/dist/skills/dag.js +181 -0
  70. package/dist/skills/index.js +5 -0
  71. package/dist/skills/registry.js +40 -0
  72. package/dist/skills/types.js +1 -0
  73. package/dist/storage/archive.js +77 -0
  74. package/dist/storage/checkpoint.js +119 -0
  75. package/dist/storage/types.js +1 -0
  76. package/dist/utils/config.js +81 -0
  77. package/dist/utils/logger.js +49 -0
  78. package/dist/version.js +1 -0
  79. package/package.json +37 -0
@@ -0,0 +1,456 @@
1
+ /**
2
+ * Psychological Analysis - Self-Reflection and Mental State Analysis
3
+ *
4
+ * Based on psychology research and HeartFlow's psychological analysis capabilities
5
+ *
6
+ * Key features:
7
+ * - Emotional state tracking
8
+ * - Cognitive bias detection
9
+ * - Self-reflection analysis
10
+ * - Mental health indicators
11
+ * - Identity consistency tracking
12
+ * - Psychological resilience metrics
13
+ */
14
+ import { randomUUID } from 'crypto';
15
+ // Common cognitive biases with descriptions
16
+ const KNOWN_BIASES = [
17
+ {
18
+ type: 'confirmation_bias',
19
+ description: '倾向寻找证实自己观点的信息,忽略反对证据',
20
+ severity: 'medium',
21
+ mitigation: '主动寻找反面证据和不同观点',
22
+ },
23
+ {
24
+ type: 'availability_heuristic',
25
+ description: '过度依赖容易想到的例子来做判断',
26
+ severity: 'low',
27
+ mitigation: '使用数据和统计信息来补充直觉',
28
+ },
29
+ {
30
+ type: 'anchoring_bias',
31
+ description: '过度依赖第一个获得的信息',
32
+ severity: 'medium',
33
+ mitigation: '考虑多个起始点,避免锚定效应',
34
+ },
35
+ {
36
+ type: 'overconfidence',
37
+ description: '高估自己的知识或能力',
38
+ severity: 'medium',
39
+ mitigation: '寻求外部反馈,承认不确定性',
40
+ },
41
+ {
42
+ type: 'recency_bias',
43
+ description: '过度关注最近的事件,忽略历史信息',
44
+ severity: 'low',
45
+ mitigation: '回顾长期趋势和历史数据',
46
+ },
47
+ {
48
+ type: 'negativity_bias',
49
+ description: '对负面信息比正面信息更敏感',
50
+ severity: 'medium',
51
+ mitigation: '平衡正面和负面事件的权重',
52
+ },
53
+ {
54
+ type: 'loss_aversion',
55
+ description: '对损失的厌恶超过对同等收益的渴望',
56
+ severity: 'medium',
57
+ mitigation: '客观评估风险和收益',
58
+ },
59
+ {
60
+ type: 'fundamental_attribution_error',
61
+ description: '将他人的行为归因于性格而非情境',
62
+ severity: 'low',
63
+ mitigation: '考虑情境因素和外部原因',
64
+ },
65
+ ];
66
+ const EMOTION_INDICATORS = {
67
+ positive: {
68
+ keywords: ['好', '棒', '优秀', '满意', '开心', '高兴', '不错', '很好', '太好了', 'good', 'great', 'excellent', 'happy'],
69
+ valence: 0.8,
70
+ arousal: 0.4,
71
+ },
72
+ negative: {
73
+ keywords: ['差', '糟糕', '失望', '不满', '难过', '伤心', '不好', 'bad', 'terrible', 'disappointed', 'sad'],
74
+ valence: -0.8,
75
+ arousal: 0.6,
76
+ },
77
+ neutral: {
78
+ keywords: ['一般', '普通', '正常', '平常', 'neutral', 'normal', 'average'],
79
+ valence: 0,
80
+ arousal: 0.2,
81
+ },
82
+ anxious: {
83
+ keywords: ['担心', '焦虑', '紧张', '不安', '害怕', 'worried', 'anxious', 'nervous', 'afraid', 'fear'],
84
+ valence: -0.3,
85
+ arousal: 0.8,
86
+ },
87
+ frustrated: {
88
+ keywords: ['挫败', '沮丧', '失望', '无奈', '烦恼', 'frustrated', 'annoyed', 'irritated'],
89
+ valence: -0.6,
90
+ arousal: 0.7,
91
+ },
92
+ confident: {
93
+ keywords: ['相信', '确定', '肯定', '自信', '有信心', 'confident', 'sure', 'certain', 'belief'],
94
+ valence: 0.7,
95
+ arousal: 0.5,
96
+ },
97
+ uncertain: {
98
+ keywords: ['可能', '也许', '不确定', '不太确定', 'maybe', 'perhaps', 'uncertain', 'not sure'],
99
+ valence: 0,
100
+ arousal: 0.3,
101
+ },
102
+ };
103
+ export function createPsychologicalAnalyzer() {
104
+ let state = {
105
+ reflections: [],
106
+ emotionalHistory: [],
107
+ biases: [],
108
+ profile: {
109
+ id: randomUUID(),
110
+ personalityTraits: new Map(),
111
+ cognitiveStrengths: [],
112
+ cognitiveWeaknesses: [],
113
+ emotionalPatterns: [],
114
+ resilienceScore: 0.7,
115
+ selfAwarenessScore: 0.6,
116
+ consistencyScore: 0.8,
117
+ lastUpdated: Date.now(),
118
+ },
119
+ cognitiveBiasHistory: new Map(),
120
+ };
121
+ function detectEmotionInText(text) {
122
+ const textLower = text.toLowerCase();
123
+ // Count emotion keywords
124
+ const emotionCounts = {
125
+ positive: 0,
126
+ negative: 0,
127
+ neutral: 0,
128
+ anxious: 0,
129
+ frustrated: 0,
130
+ confident: 0,
131
+ uncertain: 0,
132
+ };
133
+ for (const [emotion, data] of Object.entries(EMOTION_INDICATORS)) {
134
+ for (const keyword of data.keywords) {
135
+ if (textLower.includes(keyword)) {
136
+ emotionCounts[emotion]++;
137
+ }
138
+ }
139
+ }
140
+ // Find dominant emotion
141
+ let maxCount = 0;
142
+ let primary = 'neutral';
143
+ for (const [emotion, count] of Object.entries(emotionCounts)) {
144
+ if (count > maxCount) {
145
+ maxCount = count;
146
+ primary = emotion;
147
+ }
148
+ }
149
+ // Calculate intensity based on keyword density
150
+ const intensity = Math.min(1, maxCount / 3);
151
+ return { primary, intensity };
152
+ }
153
+ function analyzeEmotion(text, context) {
154
+ const { primary, intensity } = detectEmotionInText(text);
155
+ const emotionData = EMOTION_INDICATORS[primary];
156
+ const metrics = {
157
+ valence: emotionData.valence * intensity,
158
+ arousal: emotionData.arousal * intensity,
159
+ dominance: 0.5, // Default
160
+ primary,
161
+ intensity,
162
+ timestamp: Date.now(),
163
+ };
164
+ // Context can influence secondary emotion
165
+ if (context && context.length > 0) {
166
+ const contextEmotion = detectEmotionInText(context.join(' '));
167
+ if (contextEmotion.primary !== primary) {
168
+ metrics.secondary = contextEmotion.primary;
169
+ }
170
+ }
171
+ // Track history
172
+ state.emotionalHistory.push(metrics);
173
+ if (state.emotionalHistory.length > 100) {
174
+ state.emotionalHistory = state.emotionalHistory.slice(-100);
175
+ }
176
+ return metrics;
177
+ }
178
+ function trackEmotionalTrend(entries) {
179
+ if (entries.length < 2) {
180
+ return { trend: 'stable', delta: 0 };
181
+ }
182
+ const recent = entries.slice(-5);
183
+ const older = entries.slice(-10, -5);
184
+ const recentAvg = recent.reduce((sum, e) => sum + e.valence, 0) / recent.length;
185
+ const olderAvg = older.length > 0 ? older.reduce((sum, e) => sum + e.valence, 0) / older.length : recentAvg;
186
+ const delta = recentAvg - olderAvg;
187
+ if (delta > 0.1) {
188
+ return { trend: 'improving', delta };
189
+ }
190
+ else if (delta < -0.1) {
191
+ return { trend: 'declining', delta };
192
+ }
193
+ return { trend: 'stable', delta };
194
+ }
195
+ function detectBiases(text, context) {
196
+ const detected = [];
197
+ const textLower = text.toLowerCase();
198
+ for (const bias of KNOWN_BIASES) {
199
+ let matchCount = 0;
200
+ switch (bias.type) {
201
+ case 'confirmation_bias':
202
+ // Looking for confirmation indicators
203
+ if (textLower.includes('果然') || textLower.includes('正如') || textLower.includes('果然如此')) {
204
+ matchCount++;
205
+ }
206
+ break;
207
+ case 'availability_heuristic':
208
+ // Vivid examples
209
+ if (textLower.includes('比如') && textLower.includes('记得')) {
210
+ matchCount++;
211
+ }
212
+ break;
213
+ case 'overconfidence':
214
+ // Certainty indicators without qualification
215
+ if ((textLower.includes('一定') || textLower.includes('肯定') || textLower.includes('绝对')) &&
216
+ !textLower.includes('可能') && !textLower.includes('不确定')) {
217
+ matchCount++;
218
+ }
219
+ break;
220
+ case 'negativity_bias':
221
+ // More negative words than positive
222
+ const negCount = (textLower.match(/差|糟糕|失望|难过|伤心/g) || []).length;
223
+ const posCount = (textLower.match(/好|棒|优秀|满意|开心/g) || []).length;
224
+ if (negCount > posCount && negCount > 1) {
225
+ matchCount++;
226
+ }
227
+ break;
228
+ case 'recency_bias':
229
+ // Only focusing on recent
230
+ if (textLower.includes('最近') && !textLower.includes('过去') && !textLower.includes('总体')) {
231
+ matchCount++;
232
+ }
233
+ break;
234
+ case 'anchoring_bias':
235
+ // Relying on first number
236
+ if (text.match(/\d+.*\d+/) && textLower.includes('首先')) {
237
+ matchCount++;
238
+ }
239
+ break;
240
+ }
241
+ if (matchCount > 0) {
242
+ const detectedBias = {
243
+ ...bias,
244
+ detectedIn: [text.slice(0, 100)],
245
+ };
246
+ detected.push(detectedBias);
247
+ // Track in history
248
+ state.cognitiveBiasHistory.set(bias.type, (state.cognitiveBiasHistory.get(bias.type) || 0) + 1);
249
+ }
250
+ }
251
+ return detected;
252
+ }
253
+ function getCommonBiases() {
254
+ return KNOWN_BIASES.map(b => ({ ...b, detectedIn: [] }));
255
+ }
256
+ function reflect(trigger, content, context) {
257
+ const emotion = analyzeEmotion(content, context);
258
+ const biases = detectBiases(content, context);
259
+ const insights = [];
260
+ const selfCritique = [];
261
+ const growthAreas = [];
262
+ // Generate insights from reflection
263
+ if (emotion.primary === 'negative' || emotion.primary === 'frustrated') {
264
+ insights.push('Negative emotional state detected - consider reframing perspective');
265
+ growthAreas.push('Emotional regulation');
266
+ }
267
+ if (biases.length > 0) {
268
+ insights.push(`Detected ${biases.length} cognitive bias(es)`);
269
+ for (const bias of biases) {
270
+ selfCritique.push(`Potential ${bias.type}: ${bias.description}`);
271
+ if (bias.mitigation) {
272
+ growthAreas.push(`Address ${bias.type}: ${bias.mitigation}`);
273
+ }
274
+ }
275
+ }
276
+ // Positive reflections
277
+ if (emotion.primary === 'confident' || emotion.primary === 'positive') {
278
+ insights.push('Positive emotional state - good for learning and growth');
279
+ }
280
+ const entry = {
281
+ id: randomUUID(),
282
+ trigger,
283
+ content,
284
+ emotionalState: emotion,
285
+ insights,
286
+ selfCritique,
287
+ growthAreas,
288
+ timestamp: Date.now(),
289
+ };
290
+ state.reflections.push(entry);
291
+ if (state.reflections.length > 50) {
292
+ state.reflections = state.reflections.slice(-50);
293
+ }
294
+ return entry;
295
+ }
296
+ function getReflectionHistory(limit = 10) {
297
+ return state.reflections.slice(-limit);
298
+ }
299
+ function getSelfInsights() {
300
+ const insights = [];
301
+ if (state.reflections.length === 0) {
302
+ return ['Not enough reflection data yet'];
303
+ }
304
+ // Analyze emotional patterns
305
+ const emotions = state.reflections.map(r => r.emotionalState.primary);
306
+ const emotionCounts = new Map();
307
+ for (const e of emotions) {
308
+ emotionCounts.set(e, (emotionCounts.get(e) || 0) + 1);
309
+ }
310
+ const dominantEmotion = Array.from(emotionCounts.entries())
311
+ .sort((a, b) => b[1] - a[1])[0];
312
+ insights.push(`Dominant emotional state: ${dominantEmotion[0]} (${dominantEmotion[1]} times)`);
313
+ // Analyze growth areas
314
+ const allGrowthAreas = state.reflections.flatMap(r => r.growthAreas);
315
+ const growthCounts = new Map();
316
+ for (const g of allGrowthAreas) {
317
+ growthCounts.set(g, (growthCounts.get(g) || 0) + 1);
318
+ }
319
+ const topGrowth = Array.from(growthCounts.entries())
320
+ .sort((a, b) => b[1] - a[1])
321
+ .slice(0, 3);
322
+ if (topGrowth.length > 0) {
323
+ insights.push('Focus areas for growth:');
324
+ for (const [area, count] of topGrowth) {
325
+ insights.push(` - ${area} (mentioned ${count} times)`);
326
+ }
327
+ }
328
+ return insights;
329
+ }
330
+ function getProfile() {
331
+ return { ...state.profile };
332
+ }
333
+ function updateProfile(entry) {
334
+ const profile = state.profile;
335
+ // Update emotional patterns
336
+ profile.emotionalPatterns.push(entry.emotionalState.primary);
337
+ if (profile.emotionalPatterns.length > 20) {
338
+ profile.emotionalPatterns = profile.emotionalPatterns.slice(-20);
339
+ }
340
+ // Update self-awareness based on reflection frequency
341
+ const reflectionFrequency = state.reflections.length / 30; // Per month
342
+ profile.selfAwarenessScore = Math.min(1, reflectionFrequency * 0.5 + 0.3);
343
+ // Update resilience based on recovery from negative states
344
+ const negativeReflections = state.reflections.filter(r => r.emotionalState.primary === 'negative' || r.emotionalState.primary === 'frustrated');
345
+ if (negativeReflections.length >= 3) {
346
+ const lastNegativeIdx = state.reflections.lastIndexOf(negativeReflections[negativeReflections.length - 1]);
347
+ const afterNegative = state.reflections.slice(lastNegativeIdx + 1);
348
+ const recoveryCount = afterNegative.filter(r => r.emotionalState.primary === 'positive' || r.emotionalState.primary === 'confident').length;
349
+ profile.resilienceScore = afterNegative.length > 0 ? recoveryCount / afterNegative.length : 0.7;
350
+ }
351
+ profile.lastUpdated = Date.now();
352
+ }
353
+ function assessMentalHealth(recentReflections) {
354
+ if (recentReflections.length === 0) {
355
+ return { stress: 0.3, anxiety: 0.3, confidence: 0.6, resilience: 0.7 };
356
+ }
357
+ // Calculate stress (high negative emotion + frustrations)
358
+ const stressIndicators = recentReflections.filter(r => r.emotionalState.primary === 'negative' || r.emotionalState.primary === 'frustrated');
359
+ const stress = Math.min(1, stressIndicators.length / 5);
360
+ // Calculate anxiety
361
+ const anxietyIndicators = recentReflections.filter(r => r.emotionalState.primary === 'anxious');
362
+ const anxiety = Math.min(1, anxietyIndicators.length / 3);
363
+ // Calculate confidence
364
+ const confidentCount = recentReflections.filter(r => r.emotionalState.primary === 'confident').length;
365
+ const confidence = Math.min(1, confidentCount / 5);
366
+ // Calculate resilience (ability to recover)
367
+ const resilience = state.profile.resilienceScore;
368
+ return { stress, anxiety, confidence, resilience };
369
+ }
370
+ function analyze(text, context) {
371
+ const emotionalState = analyzeEmotion(text, context);
372
+ const detectedBiases = detectBiases(text, context);
373
+ const reflection = reflect('full_analysis', text, context);
374
+ const mentalHealth = assessMentalHealth(getReflectionHistory(10));
375
+ const recommendations = [];
376
+ if (mentalHealth.stress > 0.5) {
377
+ recommendations.push('Consider taking breaks to reduce stress');
378
+ }
379
+ if (mentalHealth.anxiety > 0.5) {
380
+ recommendations.push('Practice grounding techniques when feeling anxious');
381
+ }
382
+ if (detectedBiases.length > 0) {
383
+ recommendations.push(`Be aware of ${detectedBiases.length} cognitive biases in current thinking`);
384
+ }
385
+ if (emotionalState.primary === 'negative' && emotionalState.intensity > 0.7) {
386
+ recommendations.push('Strong negative emotions detected - consider reframing the situation');
387
+ }
388
+ if (mentalHealth.confidence < 0.4) {
389
+ recommendations.push('Low confidence detected - review past successes for validation');
390
+ }
391
+ return {
392
+ emotionalState,
393
+ detectedBiases,
394
+ reflectionInsights: reflection.insights,
395
+ mentalHealthIndicators: mentalHealth,
396
+ recommendations,
397
+ };
398
+ }
399
+ function checkIdentityConsistency(actions) {
400
+ const deviations = [];
401
+ if (actions.length < 3) {
402
+ return { consistent: true, deviations: [] };
403
+ }
404
+ // Check for value conflicts
405
+ const recentActions = actions.slice(-5);
406
+ // Simple pattern detection - major swings in approach
407
+ for (let i = 1; i < recentActions.length; i++) {
408
+ const prev = recentActions[i - 1].toLowerCase();
409
+ const curr = recentActions[i].toLowerCase();
410
+ // Check for contradictions
411
+ if ((prev.includes('应该') && curr.includes('不应该')) ||
412
+ (prev.includes('要') && curr.includes('不要'))) {
413
+ deviations.push(`Potential contradiction: "${recentActions[i - 1]}" vs "${recentActions[i]}"`);
414
+ }
415
+ }
416
+ return {
417
+ consistent: deviations.length === 0,
418
+ deviations,
419
+ };
420
+ }
421
+ function getStats() {
422
+ const avgConfidence = state.emotionalHistory.length > 0
423
+ ? state.emotionalHistory.reduce((sum, e) => sum + e.valence, 0) / state.emotionalHistory.length
424
+ : 0.5;
425
+ const emotions = state.emotionalHistory.map(e => e.primary);
426
+ const emotionCounts = new Map();
427
+ for (const e of emotions) {
428
+ emotionCounts.set(e, (emotionCounts.get(e) || 0) + 1);
429
+ }
430
+ // Emotional stability = how consistent the emotional state is
431
+ const emotionalStability = 1 - (state.emotionalHistory.length > 0
432
+ ? Math.abs(avgConfidence - 0.5) * 2
433
+ : 0);
434
+ return {
435
+ reflectionsCount: state.reflections.length,
436
+ biasesDetected: state.cognitiveBiasHistory.size,
437
+ avgConfidence: Math.max(0, Math.min(1, (avgConfidence + 1) / 2)),
438
+ emotionalStability,
439
+ };
440
+ }
441
+ return {
442
+ analyzeEmotion,
443
+ trackEmotionalTrend,
444
+ detectBiases,
445
+ getCommonBiases,
446
+ reflect,
447
+ getReflectionHistory,
448
+ getSelfInsights,
449
+ getProfile,
450
+ updateProfile,
451
+ assessMentalHealth,
452
+ analyze,
453
+ checkIdentityConsistency,
454
+ getStats,
455
+ };
456
+ }
@@ -0,0 +1 @@
1
+ export * from './analysis.js';
@@ -0,0 +1,191 @@
1
+ // Rollback Manager with Circuit Breaker Pattern
2
+ // Monitors health metrics and can rollback to previous version
3
+ import { randomUUID } from 'crypto';
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readFileSync as readFile } from 'fs';
5
+ import { join, resolve } from 'path';
6
+ const DEFAULT_CONFIG = {
7
+ maxDeclines: 3,
8
+ threshold: 5.0,
9
+ cooldownMs: 24 * 60 * 60 * 1000, // 24 hours
10
+ };
11
+ export function createRollbackManager(projectRoot, config) {
12
+ const cfg = { ...DEFAULT_CONFIG, ...config };
13
+ const dataDir = join(projectRoot, 'data');
14
+ const metricsFile = join(dataDir, 'rollback-metrics.json');
15
+ const historyFile = join(dataDir, 'rollback-history.json');
16
+ let state = {
17
+ metrics: [],
18
+ rollbackHistory: [],
19
+ lastStableVersion: null,
20
+ cooldownUntil: null,
21
+ };
22
+ function loadState() {
23
+ try {
24
+ if (existsSync(metricsFile)) {
25
+ const raw = readFileSync(metricsFile, 'utf-8');
26
+ const parsed = JSON.parse(raw);
27
+ state.metrics = parsed.metrics || [];
28
+ }
29
+ }
30
+ catch {
31
+ // Use defaults
32
+ }
33
+ try {
34
+ if (existsSync(historyFile)) {
35
+ const raw = readFileSync(historyFile, 'utf-8');
36
+ const parsed = JSON.parse(raw);
37
+ state.rollbackHistory = parsed.history || [];
38
+ state.lastStableVersion = parsed.lastStable || null;
39
+ state.cooldownUntil = parsed.cooldownUntil || null;
40
+ }
41
+ }
42
+ catch {
43
+ // Use defaults
44
+ }
45
+ }
46
+ function persistState() {
47
+ try {
48
+ if (!existsSync(dataDir)) {
49
+ mkdirSync(dataDir, { recursive: true });
50
+ }
51
+ const metricsTmp = metricsFile + '.tmp.' + randomUUID().slice(0, 8);
52
+ writeFileSync(metricsTmp, JSON.stringify({ metrics: state.metrics }, null, 2), 'utf-8');
53
+ require('fs').renameSync(metricsTmp, metricsFile);
54
+ const historyTmp = historyFile + '.tmp.' + randomUUID().slice(0, 8);
55
+ writeFileSync(historyTmp, JSON.stringify({
56
+ history: state.rollbackHistory,
57
+ lastStable: state.lastStableVersion,
58
+ cooldownUntil: state.cooldownUntil,
59
+ }, null, 2), 'utf-8');
60
+ require('fs').renameSync(historyTmp, historyFile);
61
+ }
62
+ catch {
63
+ // Persistence failure is non-fatal
64
+ }
65
+ }
66
+ // Load state on creation
67
+ loadState();
68
+ function recordMetric(type, score) {
69
+ state.metrics.push({
70
+ type,
71
+ score,
72
+ timestamp: Date.now(),
73
+ });
74
+ // Keep only last 100 entries
75
+ if (state.metrics.length > 100) {
76
+ state.metrics = state.metrics.slice(-100);
77
+ }
78
+ persistState();
79
+ }
80
+ function checkRollbackNeeded() {
81
+ // Check cooldown
82
+ if (state.cooldownUntil && Date.now() < state.cooldownUntil) {
83
+ return { needed: false, reason: 'In cooldown period' };
84
+ }
85
+ // Need at least maxDeclines metrics to make decision
86
+ if (state.metrics.length < cfg.maxDeclines) {
87
+ return { needed: false, reason: 'Insufficient metric history' };
88
+ }
89
+ // Look at last maxDeclines scores
90
+ const recentMetrics = state.metrics.slice(-cfg.maxDeclines);
91
+ // Check if all are decreasing
92
+ let allDecreasing = true;
93
+ for (let i = 1; i < recentMetrics.length; i++) {
94
+ if (recentMetrics[i].score >= recentMetrics[i - 1].score) {
95
+ allDecreasing = false;
96
+ break;
97
+ }
98
+ }
99
+ const lastScore = recentMetrics[recentMetrics.length - 1].score;
100
+ if (allDecreasing && lastScore < cfg.threshold) {
101
+ return {
102
+ needed: true,
103
+ reason: `Score declined ${cfg.maxDeclines} times consecutively (last: ${lastScore.toFixed(2)} < ${cfg.threshold})`,
104
+ };
105
+ }
106
+ return { needed: false, reason: 'Metrics stable' };
107
+ }
108
+ function performRollback() {
109
+ // Check cooldown
110
+ if (state.cooldownUntil && Date.now() < state.cooldownUntil) {
111
+ return { success: false, reason: 'In cooldown period' };
112
+ }
113
+ if (state.rollbackHistory.length === 0) {
114
+ return { success: false, reason: 'No rollback history available' };
115
+ }
116
+ // Get the last stable version entry
117
+ const lastEntry = state.rollbackHistory[state.rollbackHistory.length - 1];
118
+ // Validate path - no .. traversal, must be inside project
119
+ const resolvedPath = resolve(lastEntry.path);
120
+ const resolvedProjectRoot = resolve(projectRoot);
121
+ if (!resolvedPath.startsWith(resolvedProjectRoot)) {
122
+ return { success: false, reason: 'Path traversal detected - rejected' };
123
+ }
124
+ // Check if source file exists for backup
125
+ if (!existsSync(lastEntry.path)) {
126
+ return { success: false, reason: `Source file not found: ${lastEntry.path}` };
127
+ }
128
+ try {
129
+ // Atomic write: write to .rollback-backup.new then rename
130
+ const backupPath = lastEntry.path + '.rollback-backup.new';
131
+ const content = readFile(lastEntry.path, 'utf-8');
132
+ const tmpPath = backupPath + '.tmp.' + randomUUID().slice(0, 8);
133
+ writeFileSync(tmpPath, content, 'utf-8');
134
+ require('fs').renameSync(tmpPath, backupPath);
135
+ // Now restore from the backup
136
+ const restoreContent = readFileSync(backupPath, 'utf-8');
137
+ const restoreTmp = lastEntry.path + '.tmp.' + randomUUID().slice(0, 8);
138
+ writeFileSync(restoreTmp, restoreContent, 'utf-8');
139
+ require('fs').renameSync(restoreTmp, lastEntry.path);
140
+ // Update state
141
+ state.lastStableVersion = lastEntry.version;
142
+ state.cooldownUntil = Date.now() + cfg.cooldownMs;
143
+ // Add new rollback history entry
144
+ state.rollbackHistory.push({
145
+ version: randomUUID(),
146
+ timestamp: Date.now(),
147
+ path: lastEntry.path,
148
+ checksum: '', // Would compute actual checksum in production
149
+ });
150
+ persistState();
151
+ return { success: true, reason: `Rolled back to ${lastEntry.version}` };
152
+ }
153
+ catch (err) {
154
+ return { success: false, reason: `Rollback failed: ${err}` };
155
+ }
156
+ }
157
+ function getStats() {
158
+ // Calculate consecutive declines
159
+ let consecutiveDeclines = 0;
160
+ if (state.metrics.length >= cfg.maxDeclines) {
161
+ const recentMetrics = state.metrics.slice(-cfg.maxDeclines);
162
+ let decreasing = true;
163
+ for (let i = 1; i < recentMetrics.length; i++) {
164
+ if (recentMetrics[i].score >= recentMetrics[i - 1].score) {
165
+ decreasing = false;
166
+ break;
167
+ }
168
+ }
169
+ if (decreasing) {
170
+ consecutiveDeclines = cfg.maxDeclines;
171
+ }
172
+ }
173
+ return {
174
+ consecutiveDeclines,
175
+ lastStableVersion: state.lastStableVersion,
176
+ isInCooldown: state.cooldownUntil !== null && Date.now() < state.cooldownUntil,
177
+ cooldownUntil: state.cooldownUntil,
178
+ };
179
+ }
180
+ function triggerCooldown(durationMs) {
181
+ state.cooldownUntil = Date.now() + durationMs;
182
+ persistState();
183
+ }
184
+ return {
185
+ recordMetric,
186
+ checkRollbackNeeded,
187
+ performRollback,
188
+ getStats,
189
+ triggerCooldown,
190
+ };
191
+ }
@@ -0,0 +1 @@
1
+ export * from './privacy.js';