Claude Code commited on
Commit
d32baed
·
1 Parent(s): b0e4fe3

Claude Code: Review Cain's error handling implementation across the application. Spec

Browse files
ERROR_HANDLING_AUDIT_REPORT.md ADDED
@@ -0,0 +1,585 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Cain Error Handling Audit Report
2
+ **Date:** 2026-03-14
3
+ **Scope:** Full application error handling review
4
+ **Focus:** User-facing error messages, graceful degradation, crash loop prevention
5
+
6
+ ---
7
+
8
+ ## Executive Summary
9
+
10
+ Cain demonstrates **moderate to strong error handling** across backend components, but has **significant gaps in frontend error boundaries** and user-facing feedback mechanisms. The application handles network failures gracefully in most cases, but lacks comprehensive error reporting to end users.
11
+
12
+ **Overall Rating:** 6.5/10
13
+ - Backend Error Handling: 8/10
14
+ - Frontend Error Handling: 4/10
15
+ - User Communication: 5/10
16
+ - Crash Prevention: 7/10
17
+
18
+ ---
19
+
20
+ ## 1. Backend Error Handling Analysis
21
+
22
+ ### 1.1 Main Application Entry Point (`app.py`)
23
+
24
+ **Status:** ⚠️ INSUFFICIENT
25
+
26
+ ```python
27
+ # Current: app.py (lines 1-8)
28
+ import subprocess
29
+ import sys
30
+
31
+ if __name__ == "__main__":
32
+ print("Starting OpenClaw Sync Wrapper...")
33
+ subprocess.run([sys.executable, "scripts/sync_hf.py"], check=True)
34
+ ```
35
+
36
+ **Issues:**
37
+ - No try-catch block around subprocess execution
38
+ - No graceful shutdown on failure
39
+ - `check=True` will raise CalledProcessError without handling
40
+ - No user-friendly error message if script fails
41
+
42
+ **Recommendation:**
43
+ ```python
44
+ try:
45
+ subprocess.run([sys.executable, "scripts/sync_hf.py"], check=True)
46
+ except subprocess.CalledProcessError as e:
47
+ print(f"ERROR: Failed to start sync process (exit code {e.returncode})")
48
+ sys.exit(1)
49
+ except Exception as e:
50
+ print(f"ERROR: Unexpected error: {e}")
51
+ sys.exit(1)
52
+ ```
53
+
54
+ ### 1.2 Core Sync Handler (`scripts/sync_hf.py`)
55
+
56
+ **Status:** ✅ EXCELLENT
57
+
58
+ **Strengths:**
59
+ - Comprehensive try-catch blocks throughout
60
+ - Custom exception classes for Git operations (`GitTimeoutError`, `GitOperationError`)
61
+ - Timeout-controlled git operations with retry logic (up to 3 retries)
62
+ - Exponential backoff for retries (2^attempt, max 30s)
63
+ - Graceful degradation when git/huggingface APIs fail
64
+ - Detailed logging at each failure point
65
+
66
+ **Key Error Handling Patterns:**
67
+ ```python
68
+ # Lines 256-273: Dataset creation with graceful failure
69
+ try:
70
+ self.api.repo_info(repo_id=HF_REPO_ID, repo_type="dataset")
71
+ print(f"[SYNC] Dataset repo found: {HF_REPO_ID}")
72
+ return True
73
+ except Exception:
74
+ if not AUTO_CREATE_DATASET:
75
+ print(f"[SYNC] Dataset repo NOT found: {HF_REPO_ID}")
76
+ print(f"[SYNC] Set AUTO_CREATE_DATASET=true to auto-create.")
77
+ print(f"[SYNC] Persistence disabled (app will still run normally).")
78
+ return False
79
+ ```
80
+
81
+ ```python
82
+ # Lines 325-327: Restore failure handling
83
+ except Exception as e:
84
+ print(f"[SYNC] ✗ Restore failed: {e}")
85
+ traceback.print_exc()
86
+ ```
87
+
88
+ ### 1.3 Memory System (`memory/memory_system.py`, `memory/git_repo.py`)
89
+
90
+ **Status:** ✅ VERY GOOD
91
+
92
+ **Strengths:**
93
+ - Separate custom exception types for different failure modes
94
+ - Timeout configuration per git operation type
95
+ - Atomic writes for memory state (temp file + rename)
96
+ - Corrupt state file detection and recovery
97
+ - Detailed logging for debugging
98
+ - Local save succeeds even if remote sync fails
99
+
100
+ **Example - Corrupt Config Recovery (sync_hf.py:480-518):**
101
+ ```python
102
+ try:
103
+ with open(config_path, "r") as f:
104
+ data = json.load(f)
105
+ except (json.JSONDecodeError, ValueError) as e:
106
+ # Config is corrupt — back up and start fresh
107
+ print(f"[SYNC] Config JSON is corrupt: {e}")
108
+ backup = config_path.with_suffix(f".corrupt_{int(time.time())}")
109
+ shutil.copy2(config_path, backup)
110
+ print(f"[SYNC] Backed up corrupt config to {backup.name}")
111
+ # Create valid default structure
112
+ data = {...default_config...}
113
+ ```
114
+
115
+ ### 1.4 Office Agent Push Script (`frontend/office-agent-push.py`)
116
+
117
+ **Status:** ⚠️ MIXED
118
+
119
+ **Strengths:**
120
+ - Try-catch around API calls
121
+ - Graceful handling of 403/404 responses
122
+ - Automatic exit when removed from office
123
+
124
+ **Weaknesses:**
125
+ ```python
126
+ # Lines 73-74: Silent failure
127
+ def post_chatlog(entries):
128
+ try:
129
+ requests.post(f"{OFFICE}/api/chatlog", json={"messages": entries[-30:]}, timeout=5)
130
+ except:
131
+ pass # ❌ Silent failure - no logging
132
+ ```
133
+
134
+ **Recommendation:**
135
+ ```python
136
+ except requests.RequestException as e:
137
+ print(f"Warning: Failed to post chat log: {e}")
138
+ ```
139
+
140
+ ### 1.5 Coding Agent Extension (`extensions/coding-agent/index.ts`)
141
+
142
+ **Status:** ✅ GOOD
143
+
144
+ **Strengths:**
145
+ - Proper async error handling with try-catch
146
+ - Type-safe error handling (`e instanceof Error`)
147
+ - User-friendly error messages for common failures
148
+ - Timeout protection (5 minute CLAUDE_TIMEOUT)
149
+ - Graceful git re-clone on fetch failure
150
+
151
+ **Example (lines 190-193):**
152
+ ```typescript
153
+ } catch (e: unknown) {
154
+ const msg = e instanceof Error ? e.message : String(e);
155
+ return text(`Claude Code failed:\n${msg.slice(0, 3000)}`);
156
+ }
157
+ ```
158
+
159
+ ---
160
+
161
+ ## 2. Frontend Error Handling Analysis
162
+
163
+ ### 2.1 Main Application (`frontend/electron-standalone.html`)
164
+
165
+ **Status:** ⚠️ INSUFFICIENT
166
+
167
+ **Findings:**
168
+ - No React-style error boundary component
169
+ - Try-catch blocks exist but mostly silent failures
170
+ - Many `catch (_) {}` or `catch (e) {}` blocks that suppress errors
171
+ - No global error handler for uncaught exceptions
172
+ - localStorage operations wrapped in try-catch but no user feedback
173
+
174
+ **Problematic Patterns:**
175
+ ```javascript
176
+ // Lines 2513, 2528: Silent localStorage failures
177
+ try { localStorage.setItem('speedMode', speedMode); } catch(e) {}
178
+ try { localStorage.removeItem('assetPositionOverrides'); } catch (e) {}
179
+ ```
180
+
181
+ **Recommendation:**
182
+ ```javascript
183
+ try {
184
+ localStorage.setItem('speedMode', speedMode);
185
+ } catch(e) {
186
+ console.warn('Failed to save speed mode:', e);
187
+ // Optionally show user toast: "Settings couldn't be saved"
188
+ }
189
+ ```
190
+
191
+ ### 2.2 Network Request Error Handling
192
+
193
+ **Status:** ⚠️ INCONSISTENT
194
+
195
+ **Issues:**
196
+ - No retry logic for failed network requests
197
+ - No exponential backoff
198
+ - No timeout configuration visible
199
+ - Some fetch calls have error handling, others don't
200
+
201
+ **Example with error handling:**
202
+ ```javascript
203
+ // Lines 4294, 4311, 4328: Good pattern
204
+ fetch(...)
205
+ .then(...)
206
+ .catch(error => {
207
+ console.error('Fetch failed:', error);
208
+ // Some error handling present
209
+ });
210
+ ```
211
+
212
+ **Missing:**
213
+ - No automatic retry on 5xx errors
214
+ - No offline detection
215
+ - No user notification of network failures
216
+
217
+ ---
218
+
219
+ ## 3. Network Failure Handling
220
+
221
+ ### 3.1 HuggingFace API Interactions
222
+
223
+ **Status:** ✅ GOOD
224
+
225
+ **Strengths:**
226
+ - Timeout environment variables configured:
227
+ - `HF_HUB_DOWNLOAD_TIMEOUT=300`
228
+ - `HF_HUB_UPLOAD_TIMEOUT=600`
229
+ - Retry logic with exponential backoff in git operations
230
+ - Graceful degradation when dataset sync fails
231
+
232
+ **Timeout Configuration (sync_hf.py:16-25):**
233
+ ```python
234
+ TIMEOUT_CONFIG = {
235
+ "fetch": 60,
236
+ "pull": 90,
237
+ "push": 120,
238
+ "commit": 30,
239
+ "status": 15,
240
+ "add": 20,
241
+ "reset": 30,
242
+ "default": 45,
243
+ }
244
+ ```
245
+
246
+ ### 3.2 Dataset Sync Failures
247
+
248
+ **Status:** ✅ EXCELLENT
249
+
250
+ **Graceful Degradation:**
251
+ - Local save succeeds even if remote sync fails
252
+ - Change detection prevents unnecessary uploads
253
+ - Detailed error messages for each failure type
254
+ - No crash loops from sync failures
255
+
256
+ **Example (sync_hf.py:404-412):**
257
+ ```python
258
+ except Exception as e:
259
+ error_msg = str(e)
260
+ # Handle empty commit gracefully - this is not an error
261
+ if "No files have been modified" in error_msg or "empty commit" in error_msg.lower():
262
+ print(f"[SYNC] No changes to commit (already in sync)")
263
+ self._last_upload_hash = self._compute_files_hash()
264
+ return
265
+ print(f"[SYNC] ✗ Upload failed: {e}")
266
+ traceback.print_exc()
267
+ ```
268
+
269
+ ### 3.3 Telegram API Failures
270
+
271
+ **Status:** ✅ EXCELLENT
272
+
273
+ **Auto-Probe Mechanism (sync_hf.py:159-184):**
274
+ ```python
275
+ def probe_telegram_api(timeout: int = 8) -> str:
276
+ """Probe Telegram API endpoints and return the first reachable one."""
277
+ for base in TELEGRAM_API_BASES:
278
+ try:
279
+ resp = urllib.request.urlopen(req, timeout=timeout, context=ctx)
280
+ print(f"[TELEGRAM] ✓ Reachable: {base} (HTTP {resp.status})")
281
+ return base.rstrip("/")
282
+ except urllib.error.HTTPError as e:
283
+ # HTTP error (4xx/5xx) still means the host IS reachable
284
+ print(f"[TELEGRAM] ✓ Reachable: {base} (HTTP {e.code})")
285
+ return base.rstrip("/")
286
+ except Exception as e:
287
+ print(f"[TELEGRAM] ✗ Unreachable: {base} ({reason})")
288
+ continue
289
+ print("[TELEGRAM] WARNING: All API endpoints unreachable!")
290
+ return ""
291
+ ```
292
+
293
+ ---
294
+
295
+ ## 4. Crash Loop Prevention
296
+
297
+ ### 4.1 Conversation Loop Auto-Restart
298
+
299
+ **Status:** ✅ GOOD
300
+
301
+ **Implementation (sync_hf.py:878-911):**
302
+ ```python
303
+ def run_conversation_loop_forever():
304
+ """Launch conversation-loop with auto-restart on crash."""
305
+ while not stop_event.is_set():
306
+ print("[SYNC] Starting conversation-loop.py...")
307
+ conv_loop_proc = subprocess.Popen(...)
308
+ exit_code = conv_loop_proc.wait()
309
+ if stop_event.is_set():
310
+ break
311
+ print(f"[SYNC] conversation-loop.py exited ({exit_code}), restarting in 30s...")
312
+ time.sleep(30) # Prevent tight crash loop
313
+ ```
314
+
315
+ **Strengths:**
316
+ - 30-second delay between restart attempts
317
+ - Respects stop_event for graceful shutdown
318
+ - Logs exit codes for debugging
319
+
320
+ ### 4.2 Git Operation Retries
321
+
322
+ **Status:** ✅ EXCELLENT
323
+
324
+ **Retry with Exponential Backoff (git_repo.py:114-150):**
325
+ ```python
326
+ attempt = 0
327
+ while attempt <= self.max_retries:
328
+ try:
329
+ result = subprocess.run(...)
330
+ return result
331
+ except subprocess.TimeoutExpired as e:
332
+ if attempt < self.max_retries:
333
+ backoff = min(2 ** attempt, 30) # Exponential backoff, max 30s
334
+ logger.info(f"Retrying in {backoff}s...")
335
+ time.sleep(backoff)
336
+ else:
337
+ raise GitTimeoutError(...) from e
338
+ attempt += 1
339
+ ```
340
+
341
+ ---
342
+
343
+ ## 5. User-Facing Error Messages
344
+
345
+ ### 5.1 Current State
346
+
347
+ **Status:** ⚠️ NEEDS IMPROVEMENT
348
+
349
+ **Backend:** Console logs are detailed, but users don't see them
350
+ **Frontend:** Mostly silent failures with no user notification
351
+
352
+ **Examples of Silent Failures:**
353
+ ```javascript
354
+ // localStorage failures - silent
355
+ try { localStorage.setItem(...); } catch(e) {}
356
+
357
+ // Asset loading failures - no user feedback
358
+ .catch((e) => { console.error(e); });
359
+
360
+ // Network failures - sometimes no indication
361
+ .catch(() => {});
362
+ ```
363
+
364
+ ### 5.2 Recommended Improvements
365
+
366
+ 1. **Add Toast Notification System:**
367
+ ```javascript
368
+ function showToast(message, type = 'info') {
369
+ const toast = document.createElement('div');
370
+ toast.className = `toast toast-${type}`;
371
+ toast.textContent = message;
372
+ document.body.appendChild(toast);
373
+ setTimeout(() => toast.remove(), 5000);
374
+ }
375
+
376
+ // Usage
377
+ try {
378
+ localStorage.setItem('speedMode', speedMode);
379
+ } catch(e) {
380
+ showToast('Could not save settings. Storage may be full.', 'error');
381
+ }
382
+ ```
383
+
384
+ 2. **Add Error State to UI:**
385
+ ```javascript
386
+ // Show error indicator in corner of office
387
+ function showErrorIndicator(message) {
388
+ const indicator = document.getElementById('error-indicator');
389
+ indicator.textContent = '⚠️';
390
+ indicator.title = message;
391
+ indicator.style.display = 'block';
392
+ }
393
+ ```
394
+
395
+ 3. **Network Status Indicator:**
396
+ ```javascript
397
+ window.addEventListener('online', () => showToast('Back online', 'success'));
398
+ window.addEventListener('offline', () => showToast('Connection lost', 'error'));
399
+ ```
400
+
401
+ ---
402
+
403
+ ## 6. Missing Error Cases
404
+
405
+ ### 6.1 Critical Gaps
406
+
407
+ 1. **No global error boundary in frontend**
408
+ - Unhandled JavaScript exceptions will crash the UI
409
+ - No recovery mechanism for component failures
410
+
411
+ 2. **No offline mode**
412
+ - Application fails gracefully but doesn't indicate offline state
413
+ - No queue for pending operations when offline
414
+
415
+ 3. **Silent localStorage failures**
416
+ - Private browsing mode prevents localStorage
417
+ - No fallback or user notification
418
+
419
+ 4. **No asset loading error feedback**
420
+ - Images/sprites that fail to load show nothing
421
+ - No fallback UI
422
+
423
+ 5. **No API rate limiting handling**
424
+ - HuggingFace API rate limits not handled gracefully
425
+ - No exponential backoff for 429 responses
426
+
427
+ ### 6.2 Recommendations for Addition
428
+
429
+ | Priority | Component | Issue | Recommendation |
430
+ |----------|-----------|-------|----------------|
431
+ | HIGH | Frontend | No error boundary | Add global error boundary component |
432
+ | HIGH | Frontend | Silent localStorage | Add storage check and fallback |
433
+ | MEDIUM | Network | No retry logic | Implement retry with backoff |
434
+ | MEDIUM | Frontend | No offline indicator | Add connection status UI |
435
+ | LOW | Frontend | Asset loading | Add fallback images |
436
+
437
+ ---
438
+
439
+ ## 7. Crash Loop Analysis
440
+
441
+ ### 7.1 Potential Crash Loop Sources
442
+
443
+ **Identified Risks:**
444
+
445
+ 1. ✅ **MITIGATED:** Git operation failures
446
+ - Has retry logic with exponential backoff
447
+ - Max 3 retries before giving up
448
+
449
+ 2. ✅ **MITIGATED:** Conversation loop crashes
450
+ - Has 30-second delay between restarts
451
+ - Logs exit codes
452
+
453
+ 3. ⚠️ **POTENTIAL ISSUE:** Subprocess with `check=True` in app.py
454
+ - Will raise exception immediately
455
+ - No backoff before retry
456
+
457
+ 4. ⚠️ **POTENTIAL ISSUE:** Frontend initialization failures
458
+ - No error boundary
459
+ - Could cause page reload loop
460
+
461
+ ### 7.2 Recommendations
462
+
463
+ **app.py should be updated to:**
464
+ ```python
465
+ import time
466
+ import subprocess
467
+ import sys
468
+
469
+ MAX_RESTARTS = 3
470
+ RESTART_DELAY = 10 # seconds
471
+
472
+ for attempt in range(MAX_RESTARTS):
473
+ try:
474
+ result = subprocess.run(
475
+ [sys.executable, "scripts/sync_hf.py"],
476
+ check=True
477
+ )
478
+ sys.exit(result.returncode)
479
+ except subprocess.CalledProcessError as e:
480
+ if attempt < MAX_RESTARTS - 1:
481
+ print(f"Restart failed (attempt {attempt + 1}/{MAX_RESTARTS}), retrying in {RESTART_DELAY}s...")
482
+ time.sleep(RESTART_DELAY)
483
+ else:
484
+ print(f"ERROR: Failed after {MAX_RESTARTS} attempts")
485
+ sys.exit(1)
486
+ ```
487
+
488
+ ---
489
+
490
+ ## 8. Graceful Degradation Analysis
491
+
492
+ ### 8.1 What Works Well
493
+
494
+ | Component | Failure Mode | Degradation Behavior |
495
+ |-----------|--------------|---------------------|
496
+ | Git Sync | Network timeout | Local save succeeds, logs error |
497
+ | Dataset Upload | Repository not found | Continues normally, logs warning |
498
+ | Telegram API | All endpoints unreachable | Continues without Telegram |
499
+ | Config Load | Corrupt JSON | Backs up, creates default |
500
+ | Memory Save | Git push fails | Local file saved, logs error |
501
+
502
+ ### 8.2 What Needs Improvement
503
+
504
+ | Component | Issue | Recommendation |
505
+ |-----------|-------|----------------|
506
+ | Frontend | No offline mode | Show "Offline" banner, queue operations |
507
+ | localStorage | Private mode fails | Use sessionStorage or memory fallback |
508
+ | Asset loading | No fallback images | Show placeholder graphics |
509
+ | API calls | No retry | Implement retry with backoff |
510
+
511
+ ---
512
+
513
+ ## 9. Recommendations Summary
514
+
515
+ ### High Priority
516
+
517
+ 1. **Add error boundary to frontend** (`electron-standalone.html`)
518
+ - Wrap Phaser game in error boundary
519
+ - Show user-friendly error page on crash
520
+
521
+ 2. **Fix app.py subprocess handling**
522
+ - Add try-catch around subprocess.run
523
+ - Add restart delay to prevent crash loops
524
+
525
+ 3. **Add user-facing error notifications**
526
+ - Implement toast notification system
527
+ - Show errors for critical failures
528
+
529
+ 4. **Improve localStorage error handling**
530
+ - Detect private browsing mode
531
+ - Provide fallback storage mechanism
532
+
533
+ ### Medium Priority
534
+
535
+ 5. **Add network retry logic to frontend**
536
+ - Retry failed fetch requests with exponential backoff
537
+ - Show retry indicator to user
538
+
539
+ 6. **Add offline mode support**
540
+ - Detect online/offline status
541
+ - Queue operations for when back online
542
+
543
+ 7. **Improve error message consistency**
544
+ - Standardize error message format
545
+ - Add error codes for support
546
+
547
+ ### Low Priority
548
+
549
+ 8. **Add asset loading fallbacks**
550
+ - Placeholder images for missing sprites
551
+ - Progressive loading for large assets
552
+
553
+ 9. **Add error telemetry**
554
+ - Log errors to external service
555
+ - Track error rates over time
556
+
557
+ ---
558
+
559
+ ## 10. Conclusion
560
+
561
+ Cain's error handling is **strongest in backend operations** (git, dataset sync, API calls) but **weakest in user-facing communication**. The application gracefully degrades when backend services fail, but users are often unaware of failures or their causes.
562
+
563
+ **Key Strengths:**
564
+ - Comprehensive backend error handling with retries
565
+ - Graceful degradation for network failures
566
+ - Good timeout configuration
567
+ - Detailed logging for debugging
568
+
569
+ **Key Weaknesses:**
570
+ - Silent frontend errors with no user feedback
571
+ - No error boundary to prevent UI crashes
572
+ - No offline mode or network status indication
573
+ - Silent localStorage failures
574
+
575
+ **Immediate Actions:**
576
+ 1. Fix app.py subprocess error handling
577
+ 2. Add global error boundary to frontend
578
+ 3. Implement toast notification system for errors
579
+ 4. Add network status indicator
580
+
581
+ ---
582
+
583
+ **Report Generated:** 2026-03-14
584
+ **Auditor:** Claude Code Agent
585
+ **Next Review:** After implementing high-priority recommendations
ERROR_HANDLING_QUICK_REFERENCE.md ADDED
@@ -0,0 +1,442 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Error Handling Quick Reference Guide
2
+ **For Cain Developers**
3
+
4
+ This guide provides quick examples of proper error handling patterns used in Cain.
5
+
6
+ ---
7
+
8
+ ## Python Error Handling Patterns
9
+
10
+ ### 1. Subprocess Execution with Retry
11
+
12
+ ```python
13
+ import subprocess
14
+ import time
15
+
16
+ MAX_RETRIES = 3
17
+ BASE_TIMEOUT = 30 # seconds
18
+
19
+ def run_with_retry(command, timeout=BASE_TIMEOUT):
20
+ """Run subprocess with retry and exponential backoff."""
21
+ for attempt in range(MAX_RETRIES):
22
+ try:
23
+ result = subprocess.run(
24
+ command,
25
+ timeout=timeout,
26
+ check=True,
27
+ capture_output=True,
28
+ text=True
29
+ )
30
+ return result
31
+ except subprocess.TimeoutExpired:
32
+ if attempt < MAX_RETRIES - 1:
33
+ backoff = min(2 ** attempt, 30) # Max 30s
34
+ print(f"Timeout, retrying in {backoff}s...")
35
+ time.sleep(backoff)
36
+ else:
37
+ raise
38
+ except subprocess.CalledProcessError as e:
39
+ print(f"Command failed: {e.stderr}")
40
+ raise
41
+ ```
42
+
43
+ ### 2. Network API Call with Timeout
44
+
45
+ ```python
46
+ import requests
47
+ from requests.adapters import HTTPAdapter
48
+ from requests.packages.urllib3.util.retry import Retry
49
+
50
+ def create_resilient_session():
51
+ """Create a requests session with automatic retry."""
52
+ session = requests.Session()
53
+ retry = Retry(
54
+ total=3,
55
+ backoff_factor=1,
56
+ status_forcelist=[429, 500, 502, 503, 504],
57
+ )
58
+ adapter = HTTPAdapter(max_retries=retry)
59
+ session.mount('http://', adapter)
60
+ session.mount('https://', adapter)
61
+ return session
62
+
63
+ def safe_api_call(url, timeout=30):
64
+ """Make API call with proper error handling."""
65
+ try:
66
+ response = create_resilient_session().get(url, timeout=timeout)
67
+ response.raise_for_status()
68
+ return response.json()
69
+ except requests.Timeout:
70
+ print(f"API timeout: {url}")
71
+ return None
72
+ except requests.ConnectionError:
73
+ print(f"Connection failed: {url}")
74
+ return None
75
+ except requests.HTTPError as e:
76
+ print(f"HTTP error {e.response.status_code}: {url}")
77
+ return None
78
+ ```
79
+
80
+ ### 3. File Operations with Atomic Write
81
+
82
+ ```python
83
+ import os
84
+ import json
85
+
86
+ def save_atomically(filepath, data):
87
+ """Save data atomically to prevent corruption."""
88
+ temp_path = filepath + ".tmp"
89
+ try:
90
+ # Write to temp file first
91
+ with open(temp_path, 'w') as f:
92
+ json.dump(data, f)
93
+ # Atomic rename
94
+ os.rename(temp_path, filepath)
95
+ return True
96
+ except (OSError, IOError) as e:
97
+ print(f"Failed to save {filepath}: {e}")
98
+ # Clean up temp file
99
+ if os.path.exists(temp_path):
100
+ os.unlink(temp_path)
101
+ return False
102
+
103
+ def load_with_fallback(filepath, default=None):
104
+ """Load file with corrupt data recovery."""
105
+ try:
106
+ with open(filepath, 'r') as f:
107
+ return json.load(f)
108
+ except (json.JSONDecodeError, ValueError) as e:
109
+ print(f"Corrupt file {filepath}: {e}")
110
+ # Backup corrupt file
111
+ backup_path = f"{filepath}.corrupt.{int(time.time())}"
112
+ os.rename(filepath, backup_path)
113
+ print(f"Backed up to {backup_path}")
114
+ return default if default is not None else {}
115
+ except (OSError, IOError) as e:
116
+ print(f"Cannot read {filepath}: {e}")
117
+ return default if default is not None else {}
118
+ ```
119
+
120
+ ### 4. Context Manager for Resources
121
+
122
+ ```python
123
+ from contextlib import contextmanager
124
+
125
+ @contextmanager
126
+ def safe_resource(resource):
127
+ """Context manager that guarantees cleanup."""
128
+ try:
129
+ yield resource
130
+ except Exception as e:
131
+ print(f"Error with {resource}: {e}")
132
+ raise
133
+ finally:
134
+ # Always cleanup, even on error
135
+ resource.cleanup()
136
+ ```
137
+
138
+ ---
139
+
140
+ ## JavaScript/TypeScript Error Handling Patterns
141
+
142
+ ### 1. Async Function with Timeout
143
+
144
+ ```javascript
145
+ async function withTimeout(promise, timeoutMs, errorMessage = "Operation timed out") {
146
+ const timeout = new Promise((_, reject) =>
147
+ setTimeout(() => reject(new Error(errorMessage)), timeoutMs)
148
+ );
149
+ try {
150
+ return await Promise.race([promise, timeout]);
151
+ } catch (error) {
152
+ console.error(`${errorMessage}:`, error);
153
+ throw error;
154
+ }
155
+ }
156
+
157
+ // Usage
158
+ const result = await withTimeout(
159
+ fetch('/api/data'),
160
+ 10000,
161
+ 'API request timeout'
162
+ );
163
+ ```
164
+
165
+ ### 2. Fetch with Retry and Backoff
166
+
167
+ ```javascript
168
+ async function fetchWithRetry(url, options = {}, maxRetries = 3) {
169
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
170
+ try {
171
+ const response = await fetch(url, {
172
+ ...options,
173
+ signal: AbortSignal.timeout(30000) // 30s timeout
174
+ });
175
+ if (response.ok) return response;
176
+ if (response.status >= 500 && attempt < maxRetries - 1) {
177
+ // Retry server errors with backoff
178
+ const backoff = Math.min(1000 * Math.pow(2, attempt), 10000);
179
+ console.log(`Retry ${attempt + 1}/${maxRetries} after ${backoff}ms`);
180
+ await new Promise(r => setTimeout(r, backoff));
181
+ continue;
182
+ }
183
+ return response;
184
+ } catch (error) {
185
+ if (attempt === maxRetries - 1) throw error;
186
+ const backoff = Math.min(1000 * Math.pow(2, attempt), 10000);
187
+ console.log(`Network error, retry ${attempt + 1}/${maxRetries} after ${backoff}ms`);
188
+ await new Promise(r => setTimeout(r, backoff));
189
+ }
190
+ }
191
+ }
192
+ ```
193
+
194
+ ### 3. localStorage with Fallback
195
+
196
+ ```javascript
197
+ class SafeStorage {
198
+ constructor() {
199
+ this.memory = new Map();
200
+ this.available = this._checkAvailable();
201
+ }
202
+
203
+ _checkAvailable() {
204
+ try {
205
+ const test = '__storage_test__';
206
+ localStorage.setItem(test, test);
207
+ localStorage.removeItem(test);
208
+ return true;
209
+ } catch {
210
+ return false;
211
+ }
212
+ }
213
+
214
+ setItem(key, value) {
215
+ if (this.available) {
216
+ try {
217
+ localStorage.setItem(key, value);
218
+ return true;
219
+ } catch (e) {
220
+ console.warn('localStorage failed, using memory:', e);
221
+ }
222
+ }
223
+ this.memory.set(key, value);
224
+ return false;
225
+ }
226
+
227
+ getItem(key) {
228
+ if (this.available) {
229
+ const value = localStorage.getItem(key);
230
+ if (value !== null) return value;
231
+ }
232
+ return this.memory.get(key) || null;
233
+ }
234
+ }
235
+
236
+ const storage = new SafeStorage();
237
+ ```
238
+
239
+ ### 4. Error Boundary (React-like pattern)
240
+
241
+ ```javascript
242
+ function wrapWithErrorBoundary(fn) {
243
+ return function(...args) {
244
+ try {
245
+ return fn(...args);
246
+ } catch (error) {
247
+ console.error('Error in', fn.name, ':', error);
248
+ // Show user-friendly error
249
+ showErrorToUser('An unexpected error occurred. Please refresh.');
250
+ // Optionally send to error tracking
251
+ reportError(error, fn.name, args);
252
+ return null; // or fallback value
253
+ };
254
+ };
255
+ }
256
+
257
+ // Or for async functions
258
+ async function wrapAsyncWithErrorBoundary(fn) {
259
+ return async function(...args) {
260
+ try {
261
+ return await fn(...args);
262
+ } catch (error) {
263
+ console.error('Async error in', fn.name, ':', error);
264
+ showErrorToUser('Operation failed. Please try again.');
265
+ reportError(error, fn.name, args);
266
+ return null;
267
+ }
268
+ };
269
+ }
270
+ ```
271
+
272
+ ---
273
+
274
+ ## Git Operations Error Handling
275
+
276
+ ### Pattern from Cain (git_repo.py)
277
+
278
+ ```python
279
+ import subprocess
280
+ import time
281
+ from typing import List, Optional
282
+
283
+ class GitTimeoutError(Exception):
284
+ """Git operation timed out."""
285
+ pass
286
+
287
+ class GitOperationError(Exception):
288
+ """Git operation failed."""
289
+ pass
290
+
291
+ def run_git_command(
292
+ args: List[str],
293
+ timeout: int = 45,
294
+ max_retries: int = 3,
295
+ cwd: str = "."
296
+ ) -> subprocess.CompletedProcess:
297
+ """Run git command with timeout and retry."""
298
+ attempt = 0
299
+ while attempt <= max_retries:
300
+ try:
301
+ start = time.time()
302
+ result = subprocess.run(
303
+ ["git"] + args,
304
+ cwd=cwd,
305
+ timeout=timeout,
306
+ check=True,
307
+ capture_output=True,
308
+ text=True
309
+ )
310
+ elapsed = time.time() - start
311
+ print(f"Git completed in {elapsed:.2f}s: {args[0]}")
312
+ return result
313
+
314
+ except subprocess.TimeoutExpired:
315
+ attempt += 1
316
+ if attempt <= max_retries:
317
+ backoff = min(2 ** (attempt - 1), 30)
318
+ print(f"Git timeout, retry {attempt}/{max_retries} in {backoff}s")
319
+ time.sleep(backoff)
320
+ else:
321
+ raise GitTimeoutError(f"Git timed out after {max_retries} retries")
322
+
323
+ except subprocess.CalledProcessError as e:
324
+ # Don't retry on fatal errors
325
+ if "fatal:" in (e.stderr or ""):
326
+ raise GitOperationError(f"Git fatal error: {e.stderr}")
327
+ attempt += 1
328
+ if attempt <= max_retries:
329
+ backoff = min(2 ** (attempt - 1), 30)
330
+ print(f"Git failed, retry {attempt}/{max_retries} in {backoff}s")
331
+ time.sleep(backoff)
332
+ else:
333
+ raise GitOperationError(f"Git failed: {e.stderr}")
334
+ ```
335
+
336
+ ---
337
+
338
+ ## Toast Notification System
339
+
340
+ ```javascript
341
+ // Add to electron-standalone.html
342
+ const toastContainer = document.createElement('div');
343
+ toastContainer.id = 'toast-container';
344
+ toastContainer.style.cssText = `
345
+ position: fixed;
346
+ top: 20px;
347
+ right: 20px;
348
+ z-index: 10000;
349
+ `;
350
+ document.body.appendChild(toastContainer);
351
+
352
+ function showToast(message, type = 'info', duration = 5000) {
353
+ const toast = document.createElement('div');
354
+ toast.className = `toast toast-${type}`;
355
+ toast.textContent = message;
356
+ toast.style.cssText = `
357
+ padding: 12px 20px;
358
+ margin-bottom: 10px;
359
+ border-radius: 8px;
360
+ background: ${type === 'error' ? '#e94560' : type === 'success' ? '#4caf50' : '#333'};
361
+ color: white;
362
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
363
+ animation: slideIn 0.3s ease;
364
+ `;
365
+ toastContainer.appendChild(toast);
366
+
367
+ setTimeout(() => {
368
+ toast.style.animation = 'slideOut 0.3s ease';
369
+ setTimeout(() => toast.remove(), 300);
370
+ }, duration);
371
+ }
372
+
373
+ // Usage examples
374
+ try {
375
+ localStorage.setItem('setting', value);
376
+ } catch (e) {
377
+ showToast('Could not save settings', 'error');
378
+ }
379
+
380
+ // Network status
381
+ window.addEventListener('offline', () => showToast('Connection lost', 'error'));
382
+ window.addEventListener('online', () => showToast('Back online', 'success'));
383
+ ```
384
+
385
+ ---
386
+
387
+ ## Best Practices Summary
388
+
389
+ 1. **Always handle timeouts** - Network operations should have explicit timeouts
390
+ 2. **Use atomic writes** - Write to temp file, then rename
391
+ 3. **Log before suppressing** - Never silently catch without logging
392
+ 4. **Provide user feedback** - Show toasts for user-facing errors
393
+ 5. **Retry with backoff** - Exponential backoff prevents thundering herd
394
+ 6. **Degrade gracefully** - Local should work when remote fails
395
+ 7. **Clean up resources** - Use try/finally or context managers
396
+ 8. **Validate early** - Check prerequisites before attempting operations
397
+ 9. **Use specific exceptions** - Custom exception types for different errors
398
+ 10. **Document error codes** - For support and debugging
399
+
400
+ ---
401
+
402
+ ## Common Pitfalls to Avoid
403
+
404
+ ❌ **DON'T:**
405
+ ```python
406
+ try:
407
+ risky_operation()
408
+ except:
409
+ pass # Silent failure - hard to debug!
410
+ ```
411
+
412
+ ✅ **DO:**
413
+ ```python
414
+ try:
415
+ risky_operation()
416
+ except SpecificError as e:
417
+ logger.warning(f"Operation failed: {e}")
418
+ # Handle appropriately
419
+ ```
420
+
421
+ ❌ **DON'T:**
422
+ ```python
423
+ subprocess.run(command, check=True) # Crashes on failure
424
+ ```
425
+
426
+ ✅ **DO:**
427
+ ```python
428
+ try:
429
+ result = subprocess.run(command, check=True, timeout=30)
430
+ except subprocess.TimeoutExpired:
431
+ logger.error("Command timed out")
432
+ except subprocess.CalledProcessError as e:
433
+ logger.error(f"Command failed: {e}")
434
+ ```
435
+
436
+ ---
437
+
438
+ **Remember:** Good error handling makes your application:
439
+ - More reliable (fewer crashes)
440
+ - Easier to debug (better logging)
441
+ - Better for users (clear feedback)
442
+ - Easier to maintain (predictable behavior)
app.py CHANGED
@@ -1,8 +1,46 @@
1
  import subprocess
2
  import sys
 
 
 
 
 
3
 
4
  if __name__ == "__main__":
5
  # In a generic Docker Space, this might not be executed if CMD is set in Dockerfile.
6
  # But if the user switches to generic Python SDK or wants to run it manually:
7
- print("Starting OpenClaw Sync Wrapper...")
8
- subprocess.run([sys.executable, "scripts/sync_hf.py"], check=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import subprocess
2
  import sys
3
+ import time
4
+
5
+ # Restart configuration to prevent crash loops
6
+ MAX_RESTARTS = 3
7
+ RESTART_DELAY = 10 # seconds between restart attempts
8
 
9
  if __name__ == "__main__":
10
  # In a generic Docker Space, this might not be executed if CMD is set in Dockerfile.
11
  # But if the user switches to generic Python SDK or wants to run it manually:
12
+
13
+ for attempt in range(MAX_RESTARTS):
14
+ try:
15
+ print("Starting OpenClaw Sync Wrapper...")
16
+ if attempt > 0:
17
+ print(f"Restart attempt {attempt + 1}/{MAX_RESTARTS}...")
18
+
19
+ result = subprocess.run(
20
+ [sys.executable, "scripts/sync_hf.py"],
21
+ check=True
22
+ )
23
+ # Success - exit with the subprocess's return code
24
+ sys.exit(result.returncode)
25
+
26
+ except subprocess.CalledProcessError as e:
27
+ print(f"ERROR: Sync process failed with exit code {e.returncode}", file=sys.stderr)
28
+ if attempt < MAX_RESTARTS - 1:
29
+ print(f"Retrying in {RESTART_DELAY} seconds...", file=sys.stderr)
30
+ time.sleep(RESTART_DELAY)
31
+ else:
32
+ print(f"ERROR: Failed after {MAX_RESTARTS} attempts. Giving up.", file=sys.stderr)
33
+ sys.exit(1)
34
+
35
+ except KeyboardInterrupt:
36
+ print("\nInterrupted by user.")
37
+ sys.exit(130) # Standard exit code for SIGINT
38
+
39
+ except Exception as e:
40
+ print(f"ERROR: Unexpected error: {e}", file=sys.stderr)
41
+ if attempt < MAX_RESTARTS - 1:
42
+ print(f"Retrying in {RESTART_DELAY} seconds...", file=sys.stderr)
43
+ time.sleep(RESTART_DELAY)
44
+ else:
45
+ print(f"ERROR: Failed after {MAX_RESTARTS} attempts. Giving up.", file=sys.stderr)
46
+ sys.exit(1)