claude-memory-agent 2.0.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.
- package/.env.example +107 -0
- package/README.md +200 -0
- package/agent_card.py +512 -0
- package/bin/cli.js +181 -0
- package/bin/postinstall.js +216 -0
- package/config.py +104 -0
- package/dashboard.html +2689 -0
- package/hooks/README.md +196 -0
- package/hooks/__pycache__/auto-detect-response.cpython-312.pyc +0 -0
- package/hooks/__pycache__/auto_capture.cpython-312.pyc +0 -0
- package/hooks/__pycache__/session_end.cpython-312.pyc +0 -0
- package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
- package/hooks/auto-detect-response.py +348 -0
- package/hooks/auto_capture.py +255 -0
- package/hooks/detect-correction.py +173 -0
- package/hooks/grounding-hook.py +348 -0
- package/hooks/log-tool-use.py +234 -0
- package/hooks/log-user-request.py +208 -0
- package/hooks/pre-tool-decision.py +218 -0
- package/hooks/problem-detector.py +343 -0
- package/hooks/session_end.py +192 -0
- package/hooks/session_start.py +227 -0
- package/install.py +887 -0
- package/main.py +2859 -0
- package/manager.py +997 -0
- package/package.json +55 -0
- package/requirements.txt +8 -0
- package/run_server.py +136 -0
- package/services/__init__.py +50 -0
- package/services/__pycache__/__init__.cpython-312.pyc +0 -0
- package/services/__pycache__/agent_registry.cpython-312.pyc +0 -0
- package/services/__pycache__/auth.cpython-312.pyc +0 -0
- package/services/__pycache__/auto_inject.cpython-312.pyc +0 -0
- package/services/__pycache__/claude_md_sync.cpython-312.pyc +0 -0
- package/services/__pycache__/cleanup.cpython-312.pyc +0 -0
- package/services/__pycache__/compaction_flush.cpython-312.pyc +0 -0
- package/services/__pycache__/confidence.cpython-312.pyc +0 -0
- package/services/__pycache__/daily_log.cpython-312.pyc +0 -0
- package/services/__pycache__/database.cpython-312.pyc +0 -0
- package/services/__pycache__/embeddings.cpython-312.pyc +0 -0
- package/services/__pycache__/insights.cpython-312.pyc +0 -0
- package/services/__pycache__/llm_analyzer.cpython-312.pyc +0 -0
- package/services/__pycache__/memory_md_sync.cpython-312.pyc +0 -0
- package/services/__pycache__/retry_queue.cpython-312.pyc +0 -0
- package/services/__pycache__/timeline.cpython-312.pyc +0 -0
- package/services/__pycache__/vector_index.cpython-312.pyc +0 -0
- package/services/__pycache__/websocket.cpython-312.pyc +0 -0
- package/services/agent_registry.py +753 -0
- package/services/auth.py +331 -0
- package/services/auto_inject.py +250 -0
- package/services/claude_md_sync.py +275 -0
- package/services/cleanup.py +667 -0
- package/services/compaction_flush.py +447 -0
- package/services/confidence.py +301 -0
- package/services/daily_log.py +333 -0
- package/services/database.py +2485 -0
- package/services/embeddings.py +358 -0
- package/services/insights.py +632 -0
- package/services/llm_analyzer.py +595 -0
- package/services/memory_md_sync.py +409 -0
- package/services/retry_queue.py +453 -0
- package/services/timeline.py +579 -0
- package/services/vector_index.py +398 -0
- package/services/websocket.py +257 -0
- package/skills/__init__.py +6 -0
- package/skills/__pycache__/__init__.cpython-312.pyc +0 -0
- package/skills/__pycache__/admin.cpython-312.pyc +0 -0
- package/skills/__pycache__/checkpoint.cpython-312.pyc +0 -0
- package/skills/__pycache__/claude_md.cpython-312.pyc +0 -0
- package/skills/__pycache__/cleanup.cpython-312.pyc +0 -0
- package/skills/__pycache__/grounding.cpython-312.pyc +0 -0
- package/skills/__pycache__/insights.cpython-312.pyc +0 -0
- package/skills/__pycache__/natural_language.cpython-312.pyc +0 -0
- package/skills/__pycache__/retrieve.cpython-312.pyc +0 -0
- package/skills/__pycache__/search.cpython-312.pyc +0 -0
- package/skills/__pycache__/state.cpython-312.pyc +0 -0
- package/skills/__pycache__/store.cpython-312.pyc +0 -0
- package/skills/__pycache__/summarize.cpython-312.pyc +0 -0
- package/skills/__pycache__/timeline.cpython-312.pyc +0 -0
- package/skills/__pycache__/verification.cpython-312.pyc +0 -0
- package/skills/admin.py +469 -0
- package/skills/checkpoint.py +198 -0
- package/skills/claude_md.py +363 -0
- package/skills/cleanup.py +241 -0
- package/skills/grounding.py +801 -0
- package/skills/insights.py +231 -0
- package/skills/natural_language.py +277 -0
- package/skills/retrieve.py +67 -0
- package/skills/search.py +213 -0
- package/skills/state.py +182 -0
- package/skills/store.py +179 -0
- package/skills/summarize.py +588 -0
- package/skills/timeline.py +387 -0
- package/skills/verification.py +391 -0
- package/start_daemon.py +155 -0
- package/test_automation.py +221 -0
- package/test_complete.py +338 -0
- package/test_full.py +322 -0
- package/update_system.py +817 -0
- package/verify_db.py +134 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Auto-capture hook for Claude Code.
|
|
3
|
+
|
|
4
|
+
This hook automatically captures:
|
|
5
|
+
- Tool executions and their outcomes
|
|
6
|
+
- Errors encountered during sessions
|
|
7
|
+
- Decisions and their rationale
|
|
8
|
+
- File modifications
|
|
9
|
+
|
|
10
|
+
Configure in Claude Code settings:
|
|
11
|
+
{
|
|
12
|
+
"hooks": {
|
|
13
|
+
"PostToolUse": ["python /path/to/auto_capture.py"],
|
|
14
|
+
"Notification": ["python /path/to/auto_capture.py --notification"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
"""
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
import json
|
|
21
|
+
import argparse
|
|
22
|
+
import asyncio
|
|
23
|
+
import logging
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Dict, Any, Optional
|
|
27
|
+
|
|
28
|
+
# Add parent to path for imports
|
|
29
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
30
|
+
|
|
31
|
+
import httpx
|
|
32
|
+
|
|
33
|
+
# Configure logging to stderr (important for Claude Code hooks)
|
|
34
|
+
logging.basicConfig(
|
|
35
|
+
level=logging.INFO,
|
|
36
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
37
|
+
stream=sys.stderr
|
|
38
|
+
)
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
MEMORY_AGENT_URL = os.getenv("MEMORY_AGENT_URL", "http://localhost:8102")
|
|
42
|
+
API_KEY = os.getenv("MEMORY_API_KEY", "")
|
|
43
|
+
|
|
44
|
+
# Tool categories for smart capture
|
|
45
|
+
ERROR_INDICATORS = ["error", "failed", "exception", "traceback", "cannot", "unable"]
|
|
46
|
+
DECISION_INDICATORS = ["chose", "decided", "selected", "using", "approach", "strategy"]
|
|
47
|
+
IMPORTANT_TOOLS = ["Write", "Edit", "Bash", "Task"]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def should_capture(tool_name: str, output: str, exit_code: Optional[int] = None) -> tuple[bool, str, int]:
|
|
51
|
+
"""Determine if this tool execution should be captured.
|
|
52
|
+
|
|
53
|
+
Returns: (should_capture, memory_type, importance)
|
|
54
|
+
"""
|
|
55
|
+
output_lower = output.lower() if output else ""
|
|
56
|
+
|
|
57
|
+
# Always capture errors
|
|
58
|
+
if exit_code and exit_code != 0:
|
|
59
|
+
return True, "error", 8
|
|
60
|
+
|
|
61
|
+
if any(indicator in output_lower for indicator in ERROR_INDICATORS):
|
|
62
|
+
return True, "error", 7
|
|
63
|
+
|
|
64
|
+
# Capture important tool executions
|
|
65
|
+
if tool_name in IMPORTANT_TOOLS:
|
|
66
|
+
# File writes are decisions
|
|
67
|
+
if tool_name in ["Write", "Edit"]:
|
|
68
|
+
return True, "decision", 6
|
|
69
|
+
# Bash commands with meaningful output
|
|
70
|
+
if tool_name == "Bash" and len(output) > 50:
|
|
71
|
+
return True, "chunk", 5
|
|
72
|
+
# Task tool usage
|
|
73
|
+
if tool_name == "Task":
|
|
74
|
+
return True, "decision", 6
|
|
75
|
+
|
|
76
|
+
# Skip routine operations
|
|
77
|
+
if tool_name in ["Read", "Glob", "Grep"] and len(output) < 500:
|
|
78
|
+
return False, "", 0
|
|
79
|
+
|
|
80
|
+
return False, "", 0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def send_to_memory(
|
|
84
|
+
content: str,
|
|
85
|
+
memory_type: str,
|
|
86
|
+
importance: int,
|
|
87
|
+
metadata: Dict[str, Any],
|
|
88
|
+
project_path: Optional[str] = None
|
|
89
|
+
):
|
|
90
|
+
"""Send captured data to memory agent."""
|
|
91
|
+
headers = {"Content-Type": "application/json"}
|
|
92
|
+
if API_KEY:
|
|
93
|
+
headers["X-Memory-Key"] = API_KEY
|
|
94
|
+
|
|
95
|
+
payload = {
|
|
96
|
+
"jsonrpc": "2.0",
|
|
97
|
+
"method": "skills/call",
|
|
98
|
+
"params": {
|
|
99
|
+
"skill_id": "store_memory",
|
|
100
|
+
"params": {
|
|
101
|
+
"content": content,
|
|
102
|
+
"type": memory_type,
|
|
103
|
+
"importance": importance,
|
|
104
|
+
"project_path": project_path,
|
|
105
|
+
"tags": metadata.get("tags", []),
|
|
106
|
+
"metadata": metadata,
|
|
107
|
+
"outcome": metadata.get("outcome"),
|
|
108
|
+
"success": metadata.get("success", True)
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
"id": f"auto-capture-{datetime.now().isoformat()}"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
116
|
+
response = await client.post(
|
|
117
|
+
f"{MEMORY_AGENT_URL}/a2a",
|
|
118
|
+
json=payload,
|
|
119
|
+
headers=headers
|
|
120
|
+
)
|
|
121
|
+
return response.status_code == 200
|
|
122
|
+
except httpx.RequestError as e:
|
|
123
|
+
# Silently fail - don't interrupt Claude's work
|
|
124
|
+
logger.debug(f"Memory agent request failed: {e}")
|
|
125
|
+
return False
|
|
126
|
+
except httpx.HTTPStatusError as e:
|
|
127
|
+
logger.debug(f"Memory agent returned error status: {e}")
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def capture_tool_use(hook_data: Dict[str, Any]):
|
|
132
|
+
"""Capture a tool execution event."""
|
|
133
|
+
tool_name = hook_data.get("tool_name", "Unknown")
|
|
134
|
+
tool_input = hook_data.get("tool_input", {})
|
|
135
|
+
tool_output = hook_data.get("tool_output", "")
|
|
136
|
+
exit_code = hook_data.get("exit_code")
|
|
137
|
+
session_id = hook_data.get("session_id")
|
|
138
|
+
project_path = hook_data.get("project_path")
|
|
139
|
+
|
|
140
|
+
should_cap, mem_type, importance = should_capture(tool_name, tool_output, exit_code)
|
|
141
|
+
|
|
142
|
+
if not should_cap:
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
# Build content summary
|
|
146
|
+
if mem_type == "error":
|
|
147
|
+
content = f"Error in {tool_name}: {tool_output[:500]}"
|
|
148
|
+
tags = ["error", tool_name.lower(), "auto-captured"]
|
|
149
|
+
success = False
|
|
150
|
+
else:
|
|
151
|
+
# Summarize the action
|
|
152
|
+
if tool_name == "Write":
|
|
153
|
+
file_path = tool_input.get("file_path", "unknown")
|
|
154
|
+
content = f"Created/updated file: {file_path}"
|
|
155
|
+
tags = ["file-change", "auto-captured"]
|
|
156
|
+
elif tool_name == "Edit":
|
|
157
|
+
file_path = tool_input.get("file_path", "unknown")
|
|
158
|
+
content = f"Edited file: {file_path} - {tool_input.get('old_string', '')[:100]} -> {tool_input.get('new_string', '')[:100]}"
|
|
159
|
+
tags = ["file-edit", "auto-captured"]
|
|
160
|
+
elif tool_name == "Bash":
|
|
161
|
+
cmd = tool_input.get("command", "")[:200]
|
|
162
|
+
content = f"Executed: {cmd}\nResult: {tool_output[:300]}"
|
|
163
|
+
tags = ["command", "auto-captured"]
|
|
164
|
+
elif tool_name == "Task":
|
|
165
|
+
content = f"Delegated task: {tool_input.get('prompt', '')[:300]}"
|
|
166
|
+
tags = ["delegation", "auto-captured"]
|
|
167
|
+
else:
|
|
168
|
+
content = f"{tool_name}: {str(tool_input)[:200]}\nOutput: {tool_output[:300]}"
|
|
169
|
+
tags = [tool_name.lower(), "auto-captured"]
|
|
170
|
+
success = True
|
|
171
|
+
|
|
172
|
+
metadata = {
|
|
173
|
+
"tool_name": tool_name,
|
|
174
|
+
"session_id": session_id,
|
|
175
|
+
"exit_code": exit_code,
|
|
176
|
+
"auto_captured": True,
|
|
177
|
+
"timestamp": datetime.now().isoformat(),
|
|
178
|
+
"tags": tags,
|
|
179
|
+
"outcome": "success" if success else "error",
|
|
180
|
+
"success": success
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await send_to_memory(content, mem_type, importance, metadata, project_path)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
async def capture_notification(hook_data: Dict[str, Any]):
|
|
187
|
+
"""Capture a notification/error event."""
|
|
188
|
+
notification_type = hook_data.get("type", "info")
|
|
189
|
+
message = hook_data.get("message", "")
|
|
190
|
+
project_path = hook_data.get("project_path")
|
|
191
|
+
|
|
192
|
+
if notification_type == "error":
|
|
193
|
+
content = f"Session error: {message}"
|
|
194
|
+
await send_to_memory(
|
|
195
|
+
content,
|
|
196
|
+
"error",
|
|
197
|
+
8,
|
|
198
|
+
{
|
|
199
|
+
"notification_type": notification_type,
|
|
200
|
+
"auto_captured": True,
|
|
201
|
+
"timestamp": datetime.now().isoformat(),
|
|
202
|
+
"tags": ["error", "notification", "auto-captured"],
|
|
203
|
+
"success": False
|
|
204
|
+
},
|
|
205
|
+
project_path
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def read_stdin_hook_data() -> Dict[str, Any]:
|
|
210
|
+
"""Read hook data from stdin (Claude Code passes data this way)."""
|
|
211
|
+
try:
|
|
212
|
+
if not sys.stdin.isatty():
|
|
213
|
+
data = sys.stdin.read()
|
|
214
|
+
if data:
|
|
215
|
+
return json.loads(data)
|
|
216
|
+
except json.JSONDecodeError as e:
|
|
217
|
+
logger.debug(f"Failed to parse hook input JSON from stdin: {e}")
|
|
218
|
+
except (IOError, OSError) as e:
|
|
219
|
+
logger.debug(f"Failed to read from stdin: {e}")
|
|
220
|
+
return {}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def main():
|
|
224
|
+
parser = argparse.ArgumentParser(description="Auto-capture hook for Claude Code")
|
|
225
|
+
parser.add_argument("--notification", action="store_true", help="Handle notification event")
|
|
226
|
+
parser.add_argument("--test", action="store_true", help="Test mode with sample data")
|
|
227
|
+
args = parser.parse_args()
|
|
228
|
+
|
|
229
|
+
if args.test:
|
|
230
|
+
# Test with sample data
|
|
231
|
+
test_data = {
|
|
232
|
+
"tool_name": "Bash",
|
|
233
|
+
"tool_input": {"command": "python test.py"},
|
|
234
|
+
"tool_output": "Error: ModuleNotFoundError: No module named 'foo'",
|
|
235
|
+
"exit_code": 1,
|
|
236
|
+
"project_path": "/test/project"
|
|
237
|
+
}
|
|
238
|
+
asyncio.run(capture_tool_use(test_data))
|
|
239
|
+
print("Test capture sent", file=sys.stderr)
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
hook_data = read_stdin_hook_data()
|
|
243
|
+
|
|
244
|
+
if not hook_data:
|
|
245
|
+
# No data from stdin, might be direct invocation
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
if args.notification:
|
|
249
|
+
asyncio.run(capture_notification(hook_data))
|
|
250
|
+
else:
|
|
251
|
+
asyncio.run(capture_tool_use(hook_data))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
if __name__ == "__main__":
|
|
255
|
+
main()
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Correction Detection Hook for Claude Code
|
|
4
|
+
|
|
5
|
+
This script detects when the user is correcting Claude.
|
|
6
|
+
Corrections are automatically marked as anchors (verified facts).
|
|
7
|
+
|
|
8
|
+
Patterns detected:
|
|
9
|
+
- "No, it's actually..."
|
|
10
|
+
- "That's wrong, ..."
|
|
11
|
+
- "Not X, Y"
|
|
12
|
+
- "You're mistaken..."
|
|
13
|
+
- "The correct X is Y"
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import json
|
|
19
|
+
import re
|
|
20
|
+
import requests
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
# Configuration from environment
|
|
24
|
+
MEMORY_AGENT_URL = os.getenv("MEMORY_AGENT_URL", "http://localhost:8102")
|
|
25
|
+
API_TIMEOUT = int(os.getenv("API_TIMEOUT", "30"))
|
|
26
|
+
|
|
27
|
+
# Patterns that indicate a correction
|
|
28
|
+
CORRECTION_PATTERNS = [
|
|
29
|
+
r"^no[,.]?\s+(?:it'?s?|that'?s?|the)\s+(?:actually|really)?\s*(.+)",
|
|
30
|
+
r"^that'?s?\s+(?:wrong|incorrect|not right)[,.]?\s*(.+)?",
|
|
31
|
+
r"^not\s+(\w+)[,.]?\s+(?:it'?s?|but)\s+(.+)",
|
|
32
|
+
r"^you'?re?\s+(?:wrong|mistaken|incorrect)[,.]?\s*(.+)?",
|
|
33
|
+
r"^(?:the\s+)?correct\s+(?:\w+\s+)?(?:is|are|should be)\s+(.+)",
|
|
34
|
+
r"^actually[,.]?\s+(.+)",
|
|
35
|
+
r"^I\s+(?:meant|said|wanted)\s+(.+)",
|
|
36
|
+
r"^wrong[,.]?\s+(?:it'?s?|the)\s+(.+)",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
def load_session_data():
|
|
40
|
+
"""Load session data from JSON file."""
|
|
41
|
+
session_file = Path(os.getcwd()) / ".claude_session"
|
|
42
|
+
if session_file.exists():
|
|
43
|
+
content = session_file.read_text().strip()
|
|
44
|
+
try:
|
|
45
|
+
# Try JSON format first
|
|
46
|
+
return json.loads(content)
|
|
47
|
+
except json.JSONDecodeError:
|
|
48
|
+
# Fall back to legacy plain text format (just session_id)
|
|
49
|
+
return {"session_id": content}
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
def get_session_id():
|
|
53
|
+
"""Get session ID from file."""
|
|
54
|
+
data = load_session_data()
|
|
55
|
+
return data.get("session_id") if data else None
|
|
56
|
+
|
|
57
|
+
def call_memory_agent(skill_id: str, params: dict) -> dict:
|
|
58
|
+
"""Call the memory agent API."""
|
|
59
|
+
try:
|
|
60
|
+
response = requests.post(
|
|
61
|
+
f"{MEMORY_AGENT_URL}/a2a",
|
|
62
|
+
json={
|
|
63
|
+
"jsonrpc": "2.0",
|
|
64
|
+
"id": "correction-hook",
|
|
65
|
+
"method": "tasks/send",
|
|
66
|
+
"params": {
|
|
67
|
+
"message": {"parts": [{"type": "text", "text": ""}]},
|
|
68
|
+
"metadata": {
|
|
69
|
+
"skill_id": skill_id,
|
|
70
|
+
"params": params
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
timeout=API_TIMEOUT
|
|
75
|
+
)
|
|
76
|
+
return response.json()
|
|
77
|
+
except:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
def detect_correction(text: str) -> tuple[bool, str]:
|
|
81
|
+
"""
|
|
82
|
+
Detect if text is a correction and extract the correction content.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
(is_correction, correction_content)
|
|
86
|
+
"""
|
|
87
|
+
# Normalize text
|
|
88
|
+
text_lower = text.strip().lower()
|
|
89
|
+
text_clean = text.strip()
|
|
90
|
+
|
|
91
|
+
for pattern in CORRECTION_PATTERNS:
|
|
92
|
+
match = re.match(pattern, text_lower, re.IGNORECASE)
|
|
93
|
+
if match:
|
|
94
|
+
# Get the correction content
|
|
95
|
+
groups = match.groups()
|
|
96
|
+
correction = " ".join(g for g in groups if g) if groups else text_clean
|
|
97
|
+
return True, correction or text_clean
|
|
98
|
+
|
|
99
|
+
return False, ""
|
|
100
|
+
|
|
101
|
+
def main():
|
|
102
|
+
"""Detect corrections and mark as anchors."""
|
|
103
|
+
# Read hook input from stdin
|
|
104
|
+
try:
|
|
105
|
+
hook_input = json.load(sys.stdin)
|
|
106
|
+
except:
|
|
107
|
+
sys.exit(0)
|
|
108
|
+
|
|
109
|
+
# Get user message
|
|
110
|
+
user_message = hook_input.get("user_prompt", "")
|
|
111
|
+
if not user_message:
|
|
112
|
+
session_messages = hook_input.get("session_messages", [])
|
|
113
|
+
if session_messages:
|
|
114
|
+
last_msg = session_messages[-1]
|
|
115
|
+
if last_msg.get("role") == "user":
|
|
116
|
+
user_message = last_msg.get("content", "")
|
|
117
|
+
|
|
118
|
+
if not user_message:
|
|
119
|
+
sys.exit(0)
|
|
120
|
+
|
|
121
|
+
# Check if this is a correction
|
|
122
|
+
is_correction, correction_content = detect_correction(user_message)
|
|
123
|
+
|
|
124
|
+
if not is_correction:
|
|
125
|
+
sys.exit(0)
|
|
126
|
+
|
|
127
|
+
# Load session data (includes current_request_id for causal chain)
|
|
128
|
+
session_data = load_session_data()
|
|
129
|
+
if not session_data:
|
|
130
|
+
sys.exit(0)
|
|
131
|
+
|
|
132
|
+
session_id = session_data.get("session_id")
|
|
133
|
+
if not session_id:
|
|
134
|
+
sys.exit(0)
|
|
135
|
+
|
|
136
|
+
# Get the current request ID for causal chain linking
|
|
137
|
+
root_event_id = session_data.get("current_request_id")
|
|
138
|
+
|
|
139
|
+
# Mark correction as an anchor
|
|
140
|
+
call_memory_agent("mark_anchor", {
|
|
141
|
+
"session_id": session_id,
|
|
142
|
+
"fact": f"USER CORRECTION: {correction_content[:200]}",
|
|
143
|
+
"details": f"Original message: {user_message}",
|
|
144
|
+
"project_path": os.getcwd()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
# Also update the event as a clarification with causal chain linking
|
|
148
|
+
log_params = {
|
|
149
|
+
"session_id": session_id,
|
|
150
|
+
"event_type": "clarification",
|
|
151
|
+
"summary": f"User corrected: {correction_content[:150]}",
|
|
152
|
+
"details": user_message,
|
|
153
|
+
"is_anchor": True,
|
|
154
|
+
"confidence": 1.0,
|
|
155
|
+
"project_path": os.getcwd()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if root_event_id:
|
|
159
|
+
log_params["root_event_id"] = root_event_id
|
|
160
|
+
log_params["parent_event_id"] = root_event_id
|
|
161
|
+
|
|
162
|
+
call_memory_agent("timeline_log", log_params)
|
|
163
|
+
|
|
164
|
+
# Output a reminder for Claude
|
|
165
|
+
print(f"[CORRECTION DETECTED - ANCHOR CREATED]")
|
|
166
|
+
print(f"The user corrected you: {correction_content[:200]}")
|
|
167
|
+
print(f"This is now a verified fact. Do not contradict it.")
|
|
168
|
+
print("")
|
|
169
|
+
|
|
170
|
+
sys.exit(0)
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__":
|
|
173
|
+
main()
|