@goodandready/dsh-goal 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -51,6 +51,7 @@ export class GoalEngine {
51
51
  this.goals = new Map();
52
52
  this.listeners = new Set();
53
53
  this.saveTimer = null;
54
+ this.stallCounters = new Map(); // sessionId -> number of consecutive turns without progress
54
55
 
55
56
  const defaultStorageDir = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
56
57
  this.storagePath = options.storagePath ?? null;
@@ -77,15 +78,45 @@ export class GoalEngine {
77
78
  const raw = fs.readFileSync(this.storagePath, 'utf8');
78
79
  const data = JSON.parse(raw);
79
80
  if (data && typeof data === 'object') {
81
+ let dirty = false;
80
82
  if (data.sessions && typeof data.sessions === 'object') {
81
83
  for (const [sid, goal] of Object.entries(data.sessions)) {
82
84
  if (goal && goal.id && goal.title) {
85
+ // Item 4: Crash Hydration — если цель осталась в RUNNING после перезапуска/падения DSH,
86
+ // переводим в PAUSED с понятной причиной и фиксацией времени
87
+ if (goal.state === GoalState.RUNNING) {
88
+ goal.state = GoalState.PAUSED;
89
+ goal.pausedAt = Date.now();
90
+ if (!Array.isArray(goal.logs)) goal.logs = [];
91
+ goal.logs.push({
92
+ timestamp: Date.now(),
93
+ type: 'warning',
94
+ message: 'Harness was restarted — click ▶️ to resume',
95
+ });
96
+ if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
97
+ dirty = true;
98
+ }
83
99
  this.goals.set(sid, goal);
84
100
  }
85
101
  }
86
102
  } else if (data.id && data.title) {
103
+ if (data.state === GoalState.RUNNING) {
104
+ data.state = GoalState.PAUSED;
105
+ data.pausedAt = Date.now();
106
+ if (!Array.isArray(data.logs)) data.logs = [];
107
+ data.logs.push({
108
+ timestamp: Date.now(),
109
+ type: 'warning',
110
+ message: 'Harness was restarted — click ▶️ to resume',
111
+ });
112
+ if (data.logs.length > 100) data.logs = data.logs.slice(-100);
113
+ dirty = true;
114
+ }
87
115
  this.goals.set('default', data);
88
116
  }
117
+ if (dirty) {
118
+ this.scheduleSave(true);
119
+ }
89
120
  }
90
121
  }
91
122
  } catch (err) {
@@ -175,6 +206,27 @@ export class GoalEngine {
175
206
  }
176
207
  }
177
208
 
209
+ /**
210
+ * Отслеживание прогресса для Smart Progress Guard
211
+ */
212
+ recordProgress(sessionId = 'default') {
213
+ const sid = sessionId || 'default';
214
+ this.stallCounters.set(sid, 0);
215
+ }
216
+
217
+ incrementStallCount(sessionId = 'default') {
218
+ const sid = sessionId || 'default';
219
+ const current = this.stallCounters.get(sid) || 0;
220
+ const next = current + 1;
221
+ this.stallCounters.set(sid, next);
222
+ return next;
223
+ }
224
+
225
+ getStallCount(sessionId = 'default') {
226
+ const sid = sessionId || 'default';
227
+ return this.stallCounters.get(sid) || 0;
228
+ }
229
+
178
230
  /**
179
231
  * Динамическое обновление настроек на лету
180
232
  * @param {Object} config
@@ -199,7 +251,7 @@ export class GoalEngine {
199
251
  }
200
252
 
201
253
  /**
202
- * Запуск новой цели
254
+ * Получение цели сессии
203
255
  */
204
256
  getGoal(sessionId = 'default') {
205
257
  return this.goals.get(sessionId || 'default') || null;
@@ -217,11 +269,11 @@ export class GoalEngine {
217
269
  inactive.push({ sid, completedAt: goal.completedAt || goal.startedAt || 0 });
218
270
  }
219
271
  }
220
- // Сортируем от самых старых к новым
221
272
  inactive.sort((a, b) => a.completedAt - b.completedAt);
222
273
  while (this.goals.size >= this.maxSessions && inactive.length > 0) {
223
274
  const oldest = inactive.shift();
224
275
  this.goals.delete(oldest.sid);
276
+ this.stallCounters.delete(oldest.sid);
225
277
  }
226
278
  }
227
279
 
@@ -241,6 +293,7 @@ export class GoalEngine {
241
293
  const cleanTitle = title.trim();
242
294
  const now = Date.now();
243
295
  const sid = sessionId || 'default';
296
+ this.stallCounters.set(sid, 0);
244
297
 
245
298
  const goal = {
246
299
  id: `goal-${now}-${Math.random().toString(36).substring(2, 7)}`,
@@ -318,6 +371,8 @@ export class GoalEngine {
318
371
  }
319
372
 
320
373
  goal.state = GoalState.RUNNING;
374
+ this.stallCounters.set(sid, 0); // сбрасываем счетчик простоя при возобновлении
375
+
321
376
  goal.logs.push({
322
377
  timestamp: now,
323
378
  type: 'info',
@@ -362,6 +417,7 @@ export class GoalEngine {
362
417
  clear(sessionId = 'default') {
363
418
  const sid = sessionId || 'default';
364
419
  this.goals.delete(sid);
420
+ this.stallCounters.delete(sid);
365
421
  this.emit(sid, true);
366
422
  return this.getSnapshot(sid);
367
423
  }
@@ -395,6 +451,7 @@ export class GoalEngine {
395
451
  }
396
452
  }
397
453
 
454
+ this.stallCounters.delete(sid);
398
455
  this.emit(sid, true);
399
456
  return this.getSnapshot(sid);
400
457
  }
@@ -420,6 +477,7 @@ export class GoalEngine {
420
477
  });
421
478
  }
422
479
 
480
+ this.recordProgress(sid);
423
481
  if (shouldEmit) this.emit(sid);
424
482
  }
425
483
 
@@ -451,6 +509,7 @@ export class GoalEngine {
451
509
  goal.logs = goal.logs.slice(-100);
452
510
  }
453
511
 
512
+ this.recordProgress(sid);
454
513
  this.emit(sid);
455
514
  return true;
456
515
  }