claude-evolve 1.8.5 → 1.8.8

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.
@@ -295,8 +295,23 @@ get_csv_stats() {
295
295
  echo "[DISPATCHER] Starting unified evolution engine"
296
296
  echo "[DISPATCHER] Configuration: max_workers=$MAX_WORKERS, timeout=${timeout_seconds:-none}"
297
297
 
298
- # Clean up any stuck 'running' statuses at startup
298
+ # Clean up any stuck 'running' statuses and duplicates at startup
299
299
  if [[ -f "$FULL_CSV_PATH" ]]; then
300
+ echo "[DISPATCHER] Checking for duplicate candidates..."
301
+ "$PYTHON_CMD" -c "
302
+ import sys
303
+ sys.path.insert(0, '$SCRIPT_DIR/..')
304
+ from lib.evolution_csv import EvolutionCSV
305
+
306
+ try:
307
+ with EvolutionCSV('$FULL_CSV_PATH') as csv:
308
+ dup_count = csv.remove_duplicate_candidates()
309
+ if dup_count == 0:
310
+ print('[DISPATCHER] No duplicate candidates found', file=sys.stderr)
311
+ except Exception as e:
312
+ print(f'[ERROR] Failed to remove duplicates: {e}', file=sys.stderr)
313
+ " 2>&1 || true
314
+
300
315
  echo "[DISPATCHER] Resetting any stuck 'running' candidates to 'pending'..."
301
316
  if "$SCRIPT_DIR/claude-evolve-edit" running pending >/dev/null 2>&1; then
302
317
  echo "[DISPATCHER] Successfully reset stuck candidates"
@@ -583,44 +598,17 @@ while true; do
583
598
  if [[ -f "$FULL_CSV_PATH" && ${#worker_pids[@]} -eq 0 ]]; then
584
599
  echo "[DISPATCHER] Checking for stuck candidates (no active workers)..."
585
600
  "$PYTHON_CMD" -c "
586
- import csv
587
601
  import sys
588
- from pathlib import Path
589
-
590
- csv_file = '$FULL_CSV_PATH'
591
- rows = []
592
-
593
- with open(csv_file, 'r') as f:
594
- rows = list(csv.reader(f))
602
+ sys.path.insert(0, '$SCRIPT_DIR/..')
603
+ from lib.evolution_csv import EvolutionCSV
595
604
 
596
- reset_count = 0
597
- has_header = rows and rows[0] and rows[0][0].lower() == 'id'
598
- start_idx = 1 if has_header else 0
599
-
600
- for i in range(start_idx, len(rows)):
601
- if len(rows[i]) > 4:
602
- status = rows[i][4].strip() if rows[i][4] else ''
603
- candidate_id = rows[i][0] if rows[i] else ''
604
-
605
- # Reset 'running' when no workers are active
606
- if status == 'running':
607
- print(f'[INFO] Resetting stuck running candidate: {candidate_id}', file=sys.stderr)
608
- rows[i][4] = 'pending'
609
- reset_count += 1
610
- # Reset unknown statuses like 'ready'
611
- elif status not in ['', 'pending', 'complete', 'failed', 'failed-ai-retry',
612
- 'failed-retry1', 'failed-retry2', 'failed-retry3', 'skipped',
613
- 'failed-parent-missing']:
614
- print(f'[WARN] Resetting unknown status \"{status}\" to pending: {candidate_id}', file=sys.stderr)
615
- rows[i][4] = 'pending'
616
- reset_count += 1
617
-
618
- if reset_count > 0:
619
- with open(csv_file + '.tmp', 'w', newline='') as f:
620
- csv.writer(f).writerows(rows)
621
- Path(csv_file + '.tmp').rename(csv_file)
622
- print(f'[INFO] Reset {reset_count} stuck/unknown candidates to pending', file=sys.stderr)
623
- " || true
605
+ try:
606
+ with EvolutionCSV('$FULL_CSV_PATH') as csv:
607
+ reset_count = csv.reset_stuck_candidates()
608
+ except Exception as e:
609
+ print(f'[ERROR] Failed to reset stuck candidates: {e}', file=sys.stderr)
610
+ sys.exit(1)
611
+ "
624
612
  fi
625
613
  fi
626
614
 
@@ -642,22 +630,17 @@ if reset_count > 0:
642
630
  # Before blocking, do final check for stuck work (immediate, not periodic)
643
631
  echo "[DISPATCHER] Performing final verification for stuck candidates..."
644
632
  stuck_work_count=$("$PYTHON_CMD" -c "
645
- import csv
646
- csv_file = '$FULL_CSV_PATH'
647
- stuck = 0
648
- with open(csv_file, 'r') as f:
649
- reader = csv.reader(f)
650
- next(reader, None) # Skip header
651
- for row in reader:
652
- if len(row) > 4:
653
- status = row[4].strip() if row[4] else ''
654
- # Count running, unknown, and retry statuses
655
- if status in ['running'] or \
656
- (status and status not in ['', 'pending', 'complete', 'failed', 'skipped',
657
- 'failed-ai-retry', 'failed-retry1', 'failed-retry2', 'failed-retry3',
658
- 'failed-parent-missing']):
659
- stuck += 1
660
- print(stuck)
633
+ import sys
634
+ sys.path.insert(0, '$SCRIPT_DIR/..')
635
+ from lib.evolution_csv import EvolutionCSV
636
+
637
+ try:
638
+ with EvolutionCSV('$FULL_CSV_PATH') as csv:
639
+ stuck = csv.count_stuck_candidates()
640
+ print(stuck)
641
+ except Exception as e:
642
+ print(f'[ERROR] Failed to count stuck candidates: {e}', file=sys.stderr)
643
+ print('0') # Default to 0 on error
661
644
  " 2>/dev/null || echo "0")
662
645
 
663
646
  if [[ $stuck_work_count -gt 0 ]]; then
@@ -297,13 +297,22 @@ with EvolutionCSV('$FULL_CSV_PATH') as csv:
297
297
  if [[ "$current_status" == "complete" ]]; then
298
298
  echo "[WORKER-$$] Already evaluated - skipping"
299
299
  # Reset status back to complete since get_next_pending_candidate() set it to running
300
- "$PYTHON_CMD" -c "
300
+ if ! "$PYTHON_CMD" -c "
301
301
  import sys
302
302
  sys.path.insert(0, '$SCRIPT_DIR/..')
303
303
  from lib.evolution_csv import EvolutionCSV
304
304
  with EvolutionCSV('$FULL_CSV_PATH') as csv:
305
- csv.update_candidate_status('$candidate_id', 'complete')
306
- " 2>/dev/null || true
305
+ success = csv.update_candidate_status('$candidate_id', 'complete')
306
+ if not success:
307
+ print(f'ERROR: Failed to update status for $candidate_id', file=sys.stderr)
308
+ sys.exit(1)
309
+ "; then
310
+ echo "[WORKER-$$] ERROR: Failed to reset status to complete for $candidate_id" >&2
311
+ # Don't clear CURRENT_CANDIDATE_ID so cleanup handler can try again
312
+ return 1
313
+ fi
314
+
315
+ echo "[WORKER-$$] Status confirmed as complete"
307
316
  # Clear CURRENT_CANDIDATE_ID before returning to prevent cleanup from interfering
308
317
  CURRENT_CANDIDATE_ID=""
309
318
  return 0
@@ -591,11 +600,17 @@ with EvolutionCSV('$FULL_CSV_PATH') as csv:
591
600
  if rows:
592
601
  start_idx = 1 if rows and rows[0] and rows[0][0].lower() == 'id' else 0
593
602
  status_count = {}
603
+ running_candidates = []
594
604
  for row in rows[start_idx:]:
595
605
  if len(row) > 4:
596
606
  status = row[4].strip() or 'pending'
597
607
  status_count[status] = status_count.get(status, 0) + 1
608
+ if status == 'running':
609
+ candidate_id = row[0].strip().strip('\"') if len(row) > 0 else '?'
610
+ running_candidates.append(candidate_id)
598
611
  print(f'Status counts: {status_count}', file=sys.stderr)
612
+ if running_candidates:
613
+ print(f'Running candidates: {running_candidates}', file=sys.stderr)
599
614
  " 2>&1 || true
600
615
 
601
616
  # Try to claim a pending candidate
@@ -205,27 +205,33 @@ class EvolutionCSV:
205
205
  rows = self._read_csv()
206
206
  if not rows:
207
207
  return False
208
-
208
+
209
209
  updated = False
210
-
210
+ update_count = 0
211
+
211
212
  # Skip header row if it exists
212
213
  start_idx = 1 if rows and rows[0] and rows[0][0].lower() == 'id' else 0
213
-
214
+
215
+ # Update ALL matching rows (in case of duplicates)
214
216
  for i in range(start_idx, len(rows)):
215
217
  row = rows[i]
216
-
218
+
217
219
  if self.is_valid_candidate_row(row) and row[0].strip().strip('"') == candidate_id.strip().strip('"'):
218
220
  # Ensure row has at least 5 columns
219
221
  while len(row) < 5:
220
222
  row.append('')
221
-
223
+
222
224
  row[4] = new_status
223
225
  updated = True
224
- break
225
-
226
+ update_count += 1
227
+ # Don't break - continue to update ALL instances (handles duplicates)
228
+
229
+ if update_count > 1:
230
+ print(f'[WARN] Updated {update_count} duplicate entries for candidate {candidate_id} to status {new_status}', file=sys.stderr)
231
+
226
232
  if updated:
227
233
  self._write_csv(rows)
228
-
234
+
229
235
  return updated
230
236
 
231
237
  def update_candidate_performance(self, candidate_id: str, performance: str) -> bool:
@@ -233,27 +239,33 @@ class EvolutionCSV:
233
239
  rows = self._read_csv()
234
240
  if not rows:
235
241
  return False
236
-
242
+
237
243
  updated = False
238
-
244
+ update_count = 0
245
+
239
246
  # Skip header row if it exists
240
247
  start_idx = 1 if rows and rows[0] and rows[0][0].lower() == 'id' else 0
241
-
248
+
249
+ # Update ALL matching rows (in case of duplicates)
242
250
  for i in range(start_idx, len(rows)):
243
251
  row = rows[i]
244
-
252
+
245
253
  if self.is_valid_candidate_row(row) and row[0].strip().strip('"') == candidate_id.strip().strip('"'):
246
254
  # Ensure row has at least 4 columns
247
255
  while len(row) < 4:
248
256
  row.append('')
249
-
257
+
250
258
  row[3] = performance # Performance is column 4 (index 3)
251
259
  updated = True
252
- break
253
-
260
+ update_count += 1
261
+ # Don't break - continue to update ALL instances (handles duplicates)
262
+
263
+ if update_count > 1:
264
+ print(f'[WARN] Updated {update_count} duplicate entries for candidate {candidate_id} performance', file=sys.stderr)
265
+
254
266
  if updated:
255
267
  self._write_csv(rows)
256
-
268
+
257
269
  return updated
258
270
 
259
271
  def update_candidate_field(self, candidate_id: str, field_name: str, value: str) -> bool:
@@ -304,8 +316,10 @@ class EvolutionCSV:
304
316
 
305
317
  # Update the candidate's field
306
318
  updated = False
319
+ update_count = 0
307
320
  start_idx = 1 if has_header else 0
308
-
321
+
322
+ # Update ALL matching rows (in case of duplicates)
309
323
  for i in range(start_idx, len(rows)):
310
324
  row = rows[i]
311
325
  # Strip quotes from both stored ID and search ID for comparison
@@ -315,14 +329,18 @@ class EvolutionCSV:
315
329
  # Ensure row has enough columns
316
330
  while len(row) <= field_index:
317
331
  row.append('')
318
-
332
+
319
333
  row[field_index] = value
320
334
  updated = True
321
- break
322
-
335
+ update_count += 1
336
+ # Don't break - continue to update ALL instances (handles duplicates)
337
+
338
+ if update_count > 1:
339
+ print(f'[WARN] Updated {update_count} duplicate entries for candidate {candidate_id} field {field_name}', file=sys.stderr)
340
+
323
341
  if updated:
324
342
  self._write_csv(rows)
325
-
343
+
326
344
  return updated
327
345
 
328
346
  def get_candidate_info(self, candidate_id: str) -> Optional[Dict[str, str]]:
@@ -416,6 +434,126 @@ class EvolutionCSV:
416
434
 
417
435
  return fixed_count
418
436
 
437
+ def reset_stuck_candidates(self) -> int:
438
+ """
439
+ Reset 'running' candidates and unknown statuses to 'pending'.
440
+ Should only be called when no workers are active.
441
+ Returns the number of candidates reset.
442
+ """
443
+ rows = self._read_csv()
444
+ if not rows:
445
+ return 0
446
+
447
+ reset_count = 0
448
+ has_header = rows and rows[0] and rows[0][0].lower() == 'id'
449
+ start_idx = 1 if has_header else 0
450
+
451
+ valid_statuses = {'', 'pending', 'complete', 'failed', 'failed-ai-retry',
452
+ 'failed-retry1', 'failed-retry2', 'failed-retry3', 'skipped',
453
+ 'failed-parent-missing', 'running'}
454
+
455
+ # Track seen IDs to detect duplicates
456
+ seen_ids = {}
457
+
458
+ for i in range(start_idx, len(rows)):
459
+ if len(rows[i]) > 4:
460
+ status = rows[i][4].strip() if rows[i][4] else ''
461
+ candidate_id = rows[i][0].strip().strip('"') if rows[i] else ''
462
+
463
+ # Check for duplicate IDs
464
+ if candidate_id in seen_ids:
465
+ print(f'[WARN] Duplicate candidate ID found: {candidate_id} at rows {seen_ids[candidate_id]} and {i}', file=sys.stderr)
466
+ print(f'[WARN] Row {seen_ids[candidate_id]}: status={rows[seen_ids[candidate_id]][4] if len(rows[seen_ids[candidate_id]]) > 4 else "?"}', file=sys.stderr)
467
+ print(f'[WARN] Row {i}: status={status}', file=sys.stderr)
468
+ else:
469
+ seen_ids[candidate_id] = i
470
+
471
+ # Reset 'running' when no workers are active
472
+ if status == 'running':
473
+ print(f'[INFO] Resetting stuck running candidate: {candidate_id} (row {i})', file=sys.stderr)
474
+ rows[i][4] = 'pending'
475
+ reset_count += 1
476
+ # Reset unknown statuses
477
+ elif status not in valid_statuses:
478
+ print(f'[WARN] Resetting unknown status "{status}" to pending: {candidate_id}', file=sys.stderr)
479
+ rows[i][4] = 'pending'
480
+ reset_count += 1
481
+
482
+ if reset_count > 0:
483
+ self._write_csv(rows)
484
+ print(f'[INFO] Reset {reset_count} stuck/unknown candidates to pending', file=sys.stderr)
485
+
486
+ return reset_count
487
+
488
+ def count_stuck_candidates(self) -> int:
489
+ """
490
+ Count candidates that are stuck (running or have unknown status).
491
+ Returns the number of stuck candidates.
492
+ """
493
+ rows = self._read_csv()
494
+ if not rows:
495
+ return 0
496
+
497
+ stuck = 0
498
+ has_header = rows and rows[0] and rows[0][0].lower() == 'id'
499
+ start_idx = 1 if has_header else 0
500
+
501
+ valid_statuses = {'', 'pending', 'complete', 'failed', 'failed-ai-retry',
502
+ 'failed-retry1', 'failed-retry2', 'failed-retry3', 'skipped',
503
+ 'failed-parent-missing'}
504
+
505
+ for i in range(start_idx, len(rows)):
506
+ if len(rows[i]) > 4:
507
+ status = rows[i][4].strip() if rows[i][4] else ''
508
+ # Count running and unknown statuses
509
+ if status == 'running' or (status and status not in valid_statuses):
510
+ stuck += 1
511
+
512
+ return stuck
513
+
514
+ def remove_duplicate_candidates(self) -> int:
515
+ """
516
+ Remove duplicate candidate entries, keeping only the first occurrence.
517
+ Returns the number of duplicates removed.
518
+ """
519
+ rows = self._read_csv()
520
+ if not rows:
521
+ return 0
522
+
523
+ has_header = rows and rows[0] and rows[0][0].lower() == 'id'
524
+ start_idx = 1 if has_header else 0
525
+
526
+ # Track seen IDs and their row indices
527
+ seen_ids = {}
528
+ rows_to_remove = []
529
+
530
+ for i in range(start_idx, len(rows)):
531
+ if len(rows[i]) > 0:
532
+ candidate_id = rows[i][0].strip().strip('"')
533
+
534
+ if candidate_id in seen_ids:
535
+ # Duplicate found
536
+ first_row = seen_ids[candidate_id]
537
+ status_first = rows[first_row][4] if len(rows[first_row]) > 4 else ''
538
+ status_dup = rows[i][4] if len(rows[i]) > 4 else ''
539
+
540
+ print(f'[WARN] Removing duplicate candidate {candidate_id}:', file=sys.stderr)
541
+ print(f'[WARN] Keeping row {first_row}: status={status_first}', file=sys.stderr)
542
+ print(f'[WARN] Removing row {i}: status={status_dup}', file=sys.stderr)
543
+ rows_to_remove.append(i)
544
+ else:
545
+ seen_ids[candidate_id] = i
546
+
547
+ if rows_to_remove:
548
+ # Remove rows in reverse order to maintain indices
549
+ for idx in sorted(rows_to_remove, reverse=True):
550
+ del rows[idx]
551
+
552
+ self._write_csv(rows)
553
+ print(f'[INFO] Removed {len(rows_to_remove)} duplicate candidate(s)', file=sys.stderr)
554
+
555
+ return len(rows_to_remove)
556
+
419
557
  def has_pending_work(self) -> bool:
420
558
  """Check if there are any pending candidates. Used by dispatcher."""
421
559
  return self.count_pending_candidates() > 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-evolve",
3
- "version": "1.8.5",
3
+ "version": "1.8.8",
4
4
  "bin": {
5
5
  "claude-evolve": "./bin/claude-evolve",
6
6
  "claude-evolve-main": "./bin/claude-evolve-main",