claude-self-reflect 7.1.10 → 7.1.11

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.
@@ -0,0 +1,533 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Smart Event Extraction V3 - Opus-Validated Approach
4
+
5
+ Based on Opus 4.1 analysis:
6
+ 1. Single-vector approach: 500-token search index + 1000-token context cache
7
+ 2. Fixed scoring: Requests=10, Edits=9, Blocking errors=9, Build=7, Test fails=6
8
+ 3. Edit patterns instead of raw changes
9
+ 4. Conversation signature metadata for filtering
10
+ 5. Request-response pairing
11
+ """
12
+
13
+ import json
14
+ import re
15
+ from typing import Dict, List, Any, Tuple, Optional
16
+ from datetime import datetime
17
+ from collections import defaultdict
18
+
19
+
20
+ def get_message_data(msg: Dict) -> Dict:
21
+ """Extract message data handling nested structure."""
22
+ return msg.get("message", msg)
23
+
24
+
25
+ def calculate_importance_score_v3(msg: Dict, index: int, total: int) -> float:
26
+ """
27
+ Score message importance (OPUS-VALIDATED WEIGHTS).
28
+
29
+ Priority hierarchy:
30
+ 1. User requests (10pts) - The "what"
31
+ 2. Successful edits (9pts) - The "how"
32
+ 3. Blocking errors (9pts) - Critical learning moments
33
+ 4. Build success (7pts) - Validation
34
+ 5. Test failures (6pts) - Negative signals
35
+ 6. Code reads (3pts) - Intermediate steps
36
+ """
37
+ score = 0.0
38
+ msg_data = get_message_data(msg)
39
+ content = json.dumps(msg_data.get("content", "")).lower()
40
+
41
+ # User requests (the problem) - HIGHEST PRIORITY
42
+ if msg_data.get("role") == "user":
43
+ # Filter out tool_result noise
44
+ if not any(x in content for x in ["tool_result", "tool_use_id", "is_error"]):
45
+ if len(content) > 50:
46
+ score += 10.0
47
+
48
+ # Successful edits (the solution)
49
+ if isinstance(msg_data.get("content"), list):
50
+ for item in msg_data.get("content", []):
51
+ if isinstance(item, dict) and item.get("type") == "tool_use":
52
+ tool_name = str(item.get("name", "")).lower()
53
+ if "edit" in tool_name or "write" in tool_name:
54
+ if "todo" not in tool_name: # Exclude TodoWrite
55
+ score += 9.0
56
+ elif "bash" in tool_name:
57
+ score += 5.0
58
+ elif "read" in tool_name:
59
+ score += 3.0
60
+
61
+ # Blocking errors (critical learning moments)
62
+ error_keywords = ["error", "exception", "traceback", "failed", "failure", "err_"]
63
+ if any(kw in content for kw in error_keywords):
64
+ # Check if this is a blocking error (not resolved quickly)
65
+ score += 9.0
66
+
67
+ # Build/test success
68
+ if "compiled successfully" in content or "build" in content and "success" in content:
69
+ score += 7.0
70
+
71
+ # Test failures
72
+ if "test" in content and ("failed" in content or "error" in content):
73
+ score += 6.0
74
+
75
+ # Solution indicators
76
+ solution_keywords = ["fixed", "solved", "working", "success", "completed"]
77
+ if any(kw in content for kw in solution_keywords):
78
+ score += 8.0
79
+
80
+ # Position bias - beginnings and ends often important
81
+ relative_pos = index / max(total, 1)
82
+ if relative_pos < 0.1 or relative_pos > 0.8:
83
+ score *= 1.1 # Smaller boost (was 1.2)
84
+
85
+ return score
86
+
87
+
88
+ def extract_edit_pattern(messages: List[Dict], edit_index: int) -> Dict:
89
+ """
90
+ Extract edit as a reusable pattern, not just raw changes.
91
+
92
+ Opus recommendation: "Describe the reusable pattern, not specific implementation"
93
+ """
94
+ msg_data = get_message_data(messages[edit_index])
95
+
96
+ pattern = {
97
+ "index": edit_index,
98
+ "file": "unknown",
99
+ "operation_type": "unknown",
100
+ "pattern_description": "",
101
+ "why": "Unknown"
102
+ }
103
+
104
+ # Find the tool_use
105
+ content = msg_data.get("content", [])
106
+ if not isinstance(content, list):
107
+ return pattern
108
+
109
+ for item in content:
110
+ if not isinstance(item, dict) or item.get("type") != "tool_use":
111
+ continue
112
+
113
+ tool_name = item.get("name", "")
114
+ if "edit" not in tool_name.lower() and "write" not in tool_name.lower():
115
+ continue
116
+
117
+ if "todo" in tool_name.lower():
118
+ continue
119
+
120
+ # Extract file
121
+ tool_input = item.get("input", {})
122
+ pattern["file"] = tool_input.get("file_path", "unknown")
123
+
124
+ # Determine operation type from edits
125
+ if tool_name == "MultiEdit":
126
+ edits = tool_input.get("edits", [])
127
+ edit_count = len(edits)
128
+
129
+ # Analyze edit patterns
130
+ if edit_count > 5:
131
+ pattern["operation_type"] = "cascade_updates"
132
+ pattern["pattern_description"] = f"Batch operation: {edit_count} coordinated changes"
133
+ elif any("remove" in str(e).lower() or "delete" in str(e).lower() for e in edits):
134
+ pattern["operation_type"] = "removal"
135
+ pattern["pattern_description"] = "Item removal with cascade cleanup"
136
+ else:
137
+ pattern["operation_type"] = "refactor"
138
+ pattern["pattern_description"] = f"Multi-point refactoring ({edit_count} changes)"
139
+
140
+ elif tool_name == "Edit":
141
+ old_str = tool_input.get("old_string", "")
142
+ new_str = tool_input.get("new_string", "")
143
+
144
+ if len(new_str) > len(old_str) * 2:
145
+ pattern["operation_type"] = "expansion"
146
+ pattern["pattern_description"] = "Code expansion/feature addition"
147
+ elif len(new_str) < len(old_str) * 0.5:
148
+ pattern["operation_type"] = "removal"
149
+ pattern["pattern_description"] = "Code removal/simplification"
150
+ else:
151
+ pattern["operation_type"] = "modification"
152
+ pattern["pattern_description"] = "In-place modification"
153
+
154
+ elif tool_name == "Write":
155
+ pattern["operation_type"] = "creation"
156
+ pattern["pattern_description"] = "New file creation"
157
+
158
+ # Find WHY (look at recent user messages)
159
+ for j in range(max(0, edit_index-5), edit_index):
160
+ check_data = get_message_data(messages[j])
161
+ if check_data.get("role") == "user":
162
+ content_str = str(check_data.get("content", ""))
163
+ if len(content_str) > 50 and "tool_result" not in content_str:
164
+ pattern["why"] = content_str[:150]
165
+ break
166
+
167
+ return pattern
168
+
169
+ return pattern
170
+
171
+
172
+ def build_conversation_signature(messages: List[Dict], errors: List[Dict], patterns: List[Dict], metadata: Optional[Dict] = None) -> Dict:
173
+ """
174
+ Create conversation signature for filtering/search.
175
+
176
+ Opus recommendation: Enable searching "all successful React fixes" vs "debugging sessions"
177
+ """
178
+ msg_data_list = [get_message_data(m) for m in messages]
179
+
180
+ # Detect completion status
181
+ last_10 = msg_data_list[-10:]
182
+ has_build_success = any(
183
+ "compiled successfully" in str(m.get("content", "")).lower() or
184
+ ("build" in str(m.get("content", "")).lower() and "success" in str(m.get("content", "")).lower())
185
+ for m in last_10
186
+ )
187
+ has_test_success = any("test" in str(m.get("content", "")).lower() and "pass" in str(m.get("content", "")).lower() for m in last_10)
188
+ has_deployment = any("deploy" in str(m.get("content", "")).lower() and "success" in str(m.get("content", "")).lower() for m in last_10)
189
+
190
+ # Check for explicit success confirmation in final messages
191
+ has_completion_confirmation = any(
192
+ "all tasks completed" in str(m.get("content", "")).lower() or
193
+ "successfully" in str(m.get("content", "")).lower() and ("deployment" in str(m.get("content", "")).lower() or "completed" in str(m.get("content", "")).lower())
194
+ for m in last_10
195
+ )
196
+
197
+ # Only count truly blocking unresolved errors (not TodoWrite noise or empty errors)
198
+ # AND only errors in the last 20% of conversation (earlier errors may have been worked around)
199
+ last_20_percent_index = int(len(messages) * 0.8)
200
+ blocking_errors = [
201
+ e for e in errors
202
+ if not e.get("resolved", True)
203
+ and "todowrite" not in e["error_text"].lower()
204
+ and len(e["error_text"].strip()) > 20
205
+ and e["index"] > last_20_percent_index # Only recent errors are blocking
206
+ and "vercel" not in e["error_text"].lower() # Deployment errors often get worked around
207
+ and not any(url in e["error_text"] for url in ["http://", "https://"]) # URLs aren't errors
208
+ ]
209
+
210
+ if (has_build_success or has_test_success or has_deployment or has_completion_confirmation) and not blocking_errors:
211
+ completion_status = "success"
212
+ elif blocking_errors:
213
+ completion_status = "failed"
214
+ else:
215
+ completion_status = "partial"
216
+
217
+ # Detect frameworks/languages
218
+ all_content = " ".join(str(m.get("content", "")) for m in msg_data_list).lower()
219
+
220
+ frameworks = []
221
+ if "react" in all_content or "jsx" in all_content:
222
+ frameworks.append("react")
223
+ if "next" in all_content or "next.js" in all_content:
224
+ frameworks.append("nextjs")
225
+ if "typescript" in all_content or ".tsx" in all_content or ".ts" in all_content:
226
+ frameworks.append("typescript")
227
+ if "python" in all_content or ".py" in all_content:
228
+ frameworks.append("python")
229
+
230
+ # Pattern reusability
231
+ high_value_patterns = ["cascade_updates", "removal", "refactor"]
232
+ pattern_reusability = "high" if any(p["operation_type"] in high_value_patterns for p in patterns) else "medium"
233
+
234
+ # Error recovery
235
+ error_recovery = any(e.get("resolved", False) for e in errors)
236
+
237
+ signature = {
238
+ "completion_status": completion_status,
239
+ "frameworks": frameworks,
240
+ "pattern_reusability": pattern_reusability,
241
+ "error_recovery": error_recovery,
242
+ "total_edits": len(patterns),
243
+ "iteration_count": len([m for m in msg_data_list if m.get("role") == "user"])
244
+ }
245
+
246
+ # Integrate metadata if provided
247
+ if metadata:
248
+ tool_usage = metadata.get('tool_usage', {})
249
+ signature.update({
250
+ "tools_used": list(tool_usage.get('tools_summary', {}).keys())[:10],
251
+ "files_modified": [f['path'] if isinstance(f, dict) else f for f in tool_usage.get('files_edited', [])][:10],
252
+ "concepts": list(metadata.get('concepts', []))[:10],
253
+ "analysis_only": len(tool_usage.get('files_edited', [])) == 0
254
+ })
255
+
256
+ return signature
257
+
258
+
259
+ def build_search_index(messages: List[Dict], patterns: List[Dict], errors: List[Dict]) -> str:
260
+ """
261
+ Build 500-token search index optimized for keyword matching.
262
+
263
+ Opus structure:
264
+ - User request (exact words)
265
+ - Solution type + tools used
266
+ - Files modified + operation types
267
+ - Primary keywords (3-5 specific terms)
268
+ """
269
+ parts = []
270
+
271
+ # Extract user requests (exclude tool_result noise AND meta commands)
272
+ user_requests = []
273
+ for i, msg in enumerate(messages):
274
+ msg_data = get_message_data(msg)
275
+ if msg_data.get("role") == "user":
276
+ content = str(msg_data.get("content", ""))
277
+ # Skip tool results, meta commands, and system messages
278
+ if (len(content) > 50 and
279
+ "tool_result" not in content and
280
+ "tool_use_id" not in content and
281
+ "<command-name>" not in content and
282
+ "Caveat:" not in content and
283
+ "<local-command" not in content):
284
+ user_requests.append(content[:200])
285
+ if len(user_requests) >= 2: # Top 2 requests
286
+ break
287
+
288
+ if user_requests:
289
+ parts.append("## User Request")
290
+ for req in user_requests:
291
+ parts.append(req)
292
+ parts.append("")
293
+
294
+ # Edit patterns
295
+ if patterns:
296
+ parts.append("## Solution Pattern")
297
+ for p in patterns[:3]: # Top 3 patterns
298
+ file_short = p["file"].split("/")[-1] if "/" in p["file"] else p["file"]
299
+ parts.append(f"{p['operation_type']}: {file_short}")
300
+ parts.append(f" {p['pattern_description']}")
301
+ parts.append("")
302
+
303
+ # Unresolved errors only
304
+ unresolved = [e for e in errors if not e.get("resolved", True)]
305
+ if unresolved:
306
+ parts.append("## Active Issues")
307
+ for err in unresolved[:2]:
308
+ parts.append(err["error_text"][:100])
309
+ parts.append("")
310
+
311
+ return "\n".join(parts)
312
+
313
+
314
+ def build_context_cache(messages: List[Dict], patterns: List[Dict], errors: List[Dict]) -> str:
315
+ """
316
+ Build 1000-token context cache with detailed implementation.
317
+
318
+ Opus structure:
319
+ - Full edit patterns with line numbers
320
+ - Error→recovery sequences
321
+ - Build/test output snippets
322
+ - Code snippets showing the pattern
323
+ """
324
+ parts = []
325
+
326
+ # Detailed edit patterns
327
+ if patterns:
328
+ parts.append("## Implementation Details")
329
+ for p in patterns[:5]:
330
+ parts.append(f"[Msg {p['index']}] {p['operation_type']}")
331
+ parts.append(f" File: {p['file']}")
332
+ parts.append(f" Pattern: {p['pattern_description']}")
333
+ if p['why'] != "Unknown":
334
+ parts.append(f" Context: {p['why']}")
335
+ parts.append("")
336
+
337
+ # Error recovery sequences
338
+ resolved_errors = [e for e in errors if e.get("resolved", False)]
339
+ if resolved_errors:
340
+ parts.append("## Error Recovery")
341
+ for err in resolved_errors[:3]:
342
+ parts.append(f"[Msg {err['index']}] Error: {err['error_text'][:100]}")
343
+ if err.get("resolution"):
344
+ parts.append(f" Fix: {err['resolution'][:100]}")
345
+ parts.append("")
346
+
347
+ # Key validation moments
348
+ parts.append("## Validation")
349
+ for i, msg in enumerate(messages):
350
+ msg_data = get_message_data(msg)
351
+ content_str = str(msg_data.get("content", "")).lower()
352
+
353
+ if "compiled successfully" in content_str or ("build" in content_str and "success" in content_str):
354
+ parts.append(f"[Msg {i}] Build: Success")
355
+ elif "test" in content_str and "pass" in content_str:
356
+ parts.append(f"[Msg {i}] Tests: Passed")
357
+
358
+ return "\n".join(parts)
359
+
360
+
361
+ def extract_error_context(messages: List[Dict], error_index: int) -> Dict:
362
+ """Extract error with resolution tracking."""
363
+ msg_data = get_message_data(messages[error_index])
364
+ content = msg_data.get("content", "")
365
+
366
+ # Extract clean error text
367
+ if isinstance(content, list):
368
+ error_parts = []
369
+ for item in content:
370
+ if isinstance(item, dict) and item.get("type") == "tool_result":
371
+ result_content = item.get("content", "")
372
+ if "error" in str(result_content).lower():
373
+ error_parts.append(str(result_content)[:300])
374
+ elif isinstance(item, str):
375
+ error_parts.append(item[:300])
376
+ error_text = " ".join(error_parts)
377
+ else:
378
+ error_text = str(content)[:300]
379
+
380
+ # Check if resolved (explicit or implicit)
381
+ resolved = False
382
+ resolution_text = None
383
+ for i in range(error_index+1, min(len(messages), error_index+15)):
384
+ check_data = get_message_data(messages[i])
385
+ check_msg = json.dumps(check_data.get("content", "")).lower()
386
+
387
+ # Explicit resolution
388
+ if any(word in check_msg for word in ["fixed", "solved", "working"]):
389
+ resolved = True
390
+ resolution_text = check_msg[:200]
391
+ break
392
+
393
+ # Implicit resolution: success after error
394
+ if "connection_refused" in error_text.lower():
395
+ # If server starts or page loads successfully after, it's resolved
396
+ if ("background" in check_msg and "running" in check_msg) or ("playwright" in check_msg and "success" not in check_msg and "error" not in check_msg):
397
+ resolved = True
398
+ resolution_text = "Server started / page loaded successfully"
399
+ break
400
+
401
+ # Build success after build error
402
+ if ("build" in error_text.lower() or "compil" in error_text.lower()) and "compiled successfully" in check_msg:
403
+ resolved = True
404
+ resolution_text = "Build succeeded"
405
+ break
406
+
407
+ return {
408
+ "index": error_index,
409
+ "error_text": error_text,
410
+ "resolved": resolved,
411
+ "resolution": resolution_text
412
+ }
413
+
414
+
415
+ def extract_events_v3(messages: List[Dict], metadata: Optional[Dict] = None) -> Dict[str, Any]:
416
+ """
417
+ V3 extraction with Opus recommendations and optional metadata enrichment.
418
+
419
+ Args:
420
+ messages: List of conversation messages
421
+ metadata: Optional dict with {
422
+ 'tool_usage': {...}, # From extract_tool_usage_from_jsonl
423
+ 'concepts': [...] # From extract_concepts
424
+ }
425
+
426
+ Returns:
427
+ - search_index: 500 tokens for vector embedding
428
+ - context_cache: 1000 tokens for payload storage
429
+ - signature: Metadata for filtering (enriched with tool/concept data if provided)
430
+ - metadata: Original metadata passed through for use in narrative generation
431
+ """
432
+
433
+ # Score all messages with V3 weights
434
+ scores = [
435
+ (i, calculate_importance_score_v3(msg, i, len(messages)))
436
+ for i, msg in enumerate(messages)
437
+ ]
438
+ scores.sort(key=lambda x: x[1], reverse=True)
439
+ top_indices = [idx for idx, score in scores[:20]]
440
+
441
+ # Extract edit patterns
442
+ patterns = []
443
+ for i in top_indices:
444
+ msg_data = get_message_data(messages[i])
445
+ if msg_data.get("role") == "assistant":
446
+ content = msg_data.get("content", [])
447
+ if isinstance(content, list):
448
+ for item in content:
449
+ if isinstance(item, dict) and item.get("type") == "tool_use":
450
+ tool_name = item.get("name", "")
451
+ if ("edit" in tool_name.lower() or "write" in tool_name.lower()) and "todo" not in tool_name.lower():
452
+ pattern = extract_edit_pattern(messages, i)
453
+ patterns.append(pattern)
454
+
455
+ # Extract errors
456
+ errors = []
457
+ for i, msg in enumerate(messages):
458
+ msg_data = get_message_data(msg)
459
+ content_str = json.dumps(msg_data.get("content", "")).lower()
460
+ if any(kw in content_str for kw in ["error", "exception", "failed"]):
461
+ errors.append(extract_error_context(messages, i))
462
+
463
+ # Build outputs (with metadata enrichment)
464
+ search_index = build_search_index(messages, patterns, errors)
465
+ context_cache = build_context_cache(messages, patterns, errors)
466
+ signature = build_conversation_signature(messages, errors, patterns, metadata)
467
+
468
+ result = {
469
+ "search_index": search_index,
470
+ "context_cache": context_cache,
471
+ "signature": signature,
472
+ "stats": {
473
+ "original_messages": len(messages),
474
+ "search_index_tokens": len(search_index) // 4,
475
+ "context_cache_tokens": len(context_cache) // 4,
476
+ "total_tokens": (len(search_index) + len(context_cache)) // 4,
477
+ "patterns_found": len(patterns),
478
+ "errors_found": len(errors)
479
+ }
480
+ }
481
+
482
+ # Include metadata in result for narrative generation
483
+ if metadata:
484
+ result["metadata"] = metadata
485
+
486
+ return result
487
+
488
+
489
+ if __name__ == "__main__":
490
+ import sys
491
+ from pathlib import Path
492
+
493
+ if len(sys.argv) < 2:
494
+ print("Usage: python extract_events_v3.py <conversation.jsonl>")
495
+ sys.exit(1)
496
+
497
+ jsonl_path = Path(sys.argv[1])
498
+
499
+ # Read messages
500
+ messages = []
501
+ with open(jsonl_path) as f:
502
+ for line in f:
503
+ if line.strip():
504
+ messages.append(json.loads(line))
505
+
506
+ # Extract events V3
507
+ result = extract_events_v3(messages)
508
+
509
+ print(f"\n{'='*80}")
510
+ print(f"EVENT EXTRACTION V3 (OPUS-VALIDATED)")
511
+ print(f"{'='*80}\n")
512
+
513
+ print(f"Original messages: {result['stats']['original_messages']}")
514
+ print(f"Search index: {result['stats']['search_index_tokens']:,} tokens")
515
+ print(f"Context cache: {result['stats']['context_cache_tokens']:,} tokens")
516
+ print(f"Total: {result['stats']['total_tokens']:,} tokens")
517
+ print(f"Patterns found: {result['stats']['patterns_found']}")
518
+ print(f"Errors found: {result['stats']['errors_found']}")
519
+
520
+ print(f"\n{'='*80}")
521
+ print(f"CONVERSATION SIGNATURE")
522
+ print(f"{'='*80}\n")
523
+ print(json.dumps(result['signature'], indent=2))
524
+
525
+ print(f"\n{'='*80}")
526
+ print(f"SEARCH INDEX (for vector embedding)")
527
+ print(f"{'='*80}\n")
528
+ print(result['search_index'])
529
+
530
+ print(f"\n{'='*80}")
531
+ print(f"CONTEXT CACHE (for payload storage)")
532
+ print(f"{'='*80}\n")
533
+ print(result['context_cache'])