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
package/hooks/README.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Claude Code Hooks for Automatic Memory & Grounding
|
|
2
|
+
|
|
3
|
+
These hooks make the memory system **fully automatic**:
|
|
4
|
+
- Auto-capture tool executions, errors, and decisions
|
|
5
|
+
- Auto-load context at session start
|
|
6
|
+
- Auto-summarize at session end
|
|
7
|
+
- Auto-inject grounding context before every response
|
|
8
|
+
|
|
9
|
+
## How It Works
|
|
10
|
+
|
|
11
|
+
1. **UserPromptSubmit** hook fires when you send a message
|
|
12
|
+
2. `log-user-request.py` logs your message to the timeline
|
|
13
|
+
3. `grounding-hook.py` fetches current context and outputs it
|
|
14
|
+
4. Claude Code injects the output into Claude's context automatically
|
|
15
|
+
5. Claude sees the grounding context BEFORE processing your message
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
### 1. Make hooks executable (Unix/Mac)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
chmod +x ~/.claude/hooks/grounding-hook.py
|
|
23
|
+
chmod +x ~/.claude/hooks/log-user-request.py
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 2. Add to Claude Code settings
|
|
27
|
+
|
|
28
|
+
Edit `~/.claude/settings.json` and add:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"hooks": {
|
|
33
|
+
"UserPromptSubmit": [
|
|
34
|
+
{
|
|
35
|
+
"hooks": [
|
|
36
|
+
{
|
|
37
|
+
"type": "command",
|
|
38
|
+
"command": "python ~/.claude/hooks/detect-correction.py"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"type": "command",
|
|
42
|
+
"command": "python ~/.claude/hooks/log-user-request.py"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"type": "command",
|
|
46
|
+
"command": "python ~/.claude/hooks/grounding-hook.py"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"PreToolUse": [
|
|
52
|
+
{
|
|
53
|
+
"matcher": "Edit|Write|Bash|Task",
|
|
54
|
+
"hooks": [
|
|
55
|
+
{
|
|
56
|
+
"type": "command",
|
|
57
|
+
"command": "python ~/.claude/hooks/pre-tool-decision.py"
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"PostToolUse": [
|
|
63
|
+
{
|
|
64
|
+
"matcher": "Edit|Write|Bash|Read",
|
|
65
|
+
"hooks": [
|
|
66
|
+
{
|
|
67
|
+
"type": "command",
|
|
68
|
+
"command": "python ~/.claude/hooks/log-tool-use.py"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
"Stop": [
|
|
74
|
+
{
|
|
75
|
+
"hooks": [
|
|
76
|
+
{
|
|
77
|
+
"type": "command",
|
|
78
|
+
"command": "python ~/.claude/hooks/auto-detect-response.py"
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 3. Copy hooks to Claude directory
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# Create hooks directory
|
|
91
|
+
mkdir -p ~/.claude/hooks
|
|
92
|
+
|
|
93
|
+
# Copy hooks
|
|
94
|
+
cp hooks/grounding-hook.py ~/.claude/hooks/
|
|
95
|
+
cp hooks/log-user-request.py ~/.claude/hooks/
|
|
96
|
+
cp hooks/log-tool-use.py ~/.claude/hooks/
|
|
97
|
+
cp hooks/pre-tool-decision.py ~/.claude/hooks/
|
|
98
|
+
cp hooks/auto-detect-response.py ~/.claude/hooks/
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 4. Install Python dependencies
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install requests
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 5. Configure Environment Variables (Optional)
|
|
108
|
+
|
|
109
|
+
The hooks use environment variables for configuration. All have sensible defaults:
|
|
110
|
+
|
|
111
|
+
| Variable | Default | Description |
|
|
112
|
+
|----------|---------|-------------|
|
|
113
|
+
| `MEMORY_AGENT_URL` | `http://localhost:8102` | URL of the Memory Agent server |
|
|
114
|
+
| `API_TIMEOUT` | `30` | Request timeout in seconds |
|
|
115
|
+
|
|
116
|
+
Set these in your shell profile or before running Claude Code:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# In ~/.bashrc, ~/.zshrc, or equivalent
|
|
120
|
+
export MEMORY_AGENT_URL=http://localhost:8102
|
|
121
|
+
export API_TIMEOUT=30
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Or create a `.env` file (see `.env.example` for all options).
|
|
125
|
+
|
|
126
|
+
## What Gets Injected
|
|
127
|
+
|
|
128
|
+
Before every response, Claude sees:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
[GROUNDING CONTEXT - VERIFY BEFORE RESPONDING]
|
|
132
|
+
CURRENT GOAL: Fix authentication bug in login.py
|
|
133
|
+
ENTITY REGISTRY (use these exact references):
|
|
134
|
+
- auth_file: src/auth.py
|
|
135
|
+
- config: config/settings.json
|
|
136
|
+
ANCHORS (verified facts - DO NOT CONTRADICT):
|
|
137
|
+
- Bug is in the token validation function
|
|
138
|
+
- User confirmed error happens on line 45
|
|
139
|
+
RECENT DECISIONS:
|
|
140
|
+
- Use JWT tokens (not sessions)
|
|
141
|
+
RECENT EVENTS:
|
|
142
|
+
- [user_request] Fix the login bug
|
|
143
|
+
- [action] Read src/auth.py
|
|
144
|
+
- [observation] Found null check missing
|
|
145
|
+
[/GROUNDING CONTEXT]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Why This Works Better
|
|
149
|
+
|
|
150
|
+
| Approach | Problem |
|
|
151
|
+
|----------|---------|
|
|
152
|
+
| Claude calls tools | Claude forgets to call them when hallucinating |
|
|
153
|
+
| Automatic injection | Context is there whether Claude remembers or not |
|
|
154
|
+
|
|
155
|
+
The hallucination happens during generation. By injecting context BEFORE generation, we give Claude the grounding information when it matters.
|
|
156
|
+
|
|
157
|
+
## Troubleshooting
|
|
158
|
+
|
|
159
|
+
### Memory agent not running
|
|
160
|
+
The hooks will silently fail if the memory agent isn't running. Start it:
|
|
161
|
+
```bash
|
|
162
|
+
cd memory-agent
|
|
163
|
+
python main.py
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Wrong port configuration
|
|
167
|
+
Ensure `MEMORY_AGENT_URL` matches the port your server is running on:
|
|
168
|
+
```bash
|
|
169
|
+
# Check what port the server is using (default: 8102)
|
|
170
|
+
# Then set the environment variable if different:
|
|
171
|
+
export MEMORY_AGENT_URL=http://localhost:8102
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### No session file
|
|
175
|
+
The grounding hook creates a `.claude_session` file in your project directory. If you want to start fresh, delete it:
|
|
176
|
+
```bash
|
|
177
|
+
rm .claude_session
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Check if hooks are running
|
|
181
|
+
Add some debug output to see if hooks are being called:
|
|
182
|
+
```bash
|
|
183
|
+
# In grounding-hook.py, add at the start:
|
|
184
|
+
print("[DEBUG] Grounding hook called", file=sys.stderr)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Advanced: Auto-Detect from Responses
|
|
188
|
+
|
|
189
|
+
The `Stop` hook runs after Claude responds. We can use it to auto-detect decisions and observations:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
# auto-detect-response.py
|
|
193
|
+
# Parses Claude's response and logs any detected decisions/observations
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
This closes the loop - user requests are logged, context is injected, and Claude's responses are analyzed for implicit decisions.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Auto-Detect Response Hook for Claude Code
|
|
4
|
+
|
|
5
|
+
This script runs after Claude responds (Stop hook).
|
|
6
|
+
It analyzes Claude's response for decisions and observations,
|
|
7
|
+
logging them to the timeline automatically.
|
|
8
|
+
|
|
9
|
+
Also logs an 'outcome' event summarizing the result of the request.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
import logging
|
|
17
|
+
import requests
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
# Configure logging to stderr (important for Claude Code hooks)
|
|
21
|
+
logging.basicConfig(
|
|
22
|
+
level=logging.INFO,
|
|
23
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
24
|
+
stream=sys.stderr
|
|
25
|
+
)
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
# Configuration from environment
|
|
29
|
+
MEMORY_AGENT_URL = os.getenv("MEMORY_AGENT_URL", "http://localhost:8102")
|
|
30
|
+
API_TIMEOUT = int(os.getenv("API_TIMEOUT", "30"))
|
|
31
|
+
|
|
32
|
+
# Outcome detection patterns
|
|
33
|
+
OUTCOME_SUCCESS_PATTERNS = [
|
|
34
|
+
r"Done[.!]",
|
|
35
|
+
r"Completed[.!]",
|
|
36
|
+
r"Finished[.!]",
|
|
37
|
+
r"Fixed the",
|
|
38
|
+
r"Resolved the",
|
|
39
|
+
r"Successfully",
|
|
40
|
+
r"Created",
|
|
41
|
+
r"Added",
|
|
42
|
+
r"Implemented",
|
|
43
|
+
r"Updated",
|
|
44
|
+
r"I've made the changes",
|
|
45
|
+
r"The changes are complete",
|
|
46
|
+
r"Here's the",
|
|
47
|
+
r"I've updated",
|
|
48
|
+
r"I've fixed",
|
|
49
|
+
r"I've added",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
OUTCOME_FAILED_PATTERNS = [
|
|
53
|
+
r"Error:",
|
|
54
|
+
r"Failed:",
|
|
55
|
+
r"Could not",
|
|
56
|
+
r"Unable to",
|
|
57
|
+
r"I couldn't",
|
|
58
|
+
r"This won't work",
|
|
59
|
+
r"There's a problem",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
OUTCOME_PARTIAL_PATTERNS = [
|
|
63
|
+
r"Let me know if",
|
|
64
|
+
r"Should I",
|
|
65
|
+
r"Would you like me to",
|
|
66
|
+
r"I can also",
|
|
67
|
+
r"If you want",
|
|
68
|
+
r"I need more information",
|
|
69
|
+
r"Could you clarify",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
# Thinking/reasoning patterns - these indicate Claude's thought process
|
|
73
|
+
THINKING_PATTERNS = [
|
|
74
|
+
(r"Let me (\w+)", "approach"), # "Let me check...", "Let me analyze..."
|
|
75
|
+
(r"I('ll| will) (\w+)", "intent"), # "I'll start by...", "I will check..."
|
|
76
|
+
(r"First,? I", "sequence"), # "First, I need to..."
|
|
77
|
+
(r"The reason is", "explanation"), # Explaining why
|
|
78
|
+
(r"This is because", "explanation"), # Explaining cause
|
|
79
|
+
(r"Based on", "reasoning"), # "Based on the error..."
|
|
80
|
+
(r"Looking at", "analysis"), # "Looking at the code..."
|
|
81
|
+
(r"I notice", "observation"), # "I notice that..."
|
|
82
|
+
(r"It seems|It looks like", "inference"), # Making inferences
|
|
83
|
+
(r"The problem is|The issue is", "diagnosis"), # Diagnosing
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
# Decision patterns - indicate choices made
|
|
87
|
+
DECISION_PATTERNS = [
|
|
88
|
+
(r"I('ll| will) use", "tool_choice"),
|
|
89
|
+
(r"I('m going to| am going to)", "action_plan"),
|
|
90
|
+
(r"Let's (\w+)", "collaborative_decision"),
|
|
91
|
+
(r"The best approach", "strategy"),
|
|
92
|
+
(r"Instead of .+, I", "alternative_choice"),
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def extract_thinking(response_text: str) -> list:
|
|
97
|
+
"""
|
|
98
|
+
Extract thinking/reasoning segments from Claude's response.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
list: List of (thinking_type, text) tuples
|
|
102
|
+
"""
|
|
103
|
+
thinking_segments = []
|
|
104
|
+
sentences = re.split(r'[.!?]\s+', response_text)
|
|
105
|
+
|
|
106
|
+
for sentence in sentences[:10]: # Only check first 10 sentences (A* heuristic)
|
|
107
|
+
for pattern, thinking_type in THINKING_PATTERNS:
|
|
108
|
+
if re.search(pattern, sentence, re.IGNORECASE):
|
|
109
|
+
thinking_segments.append((thinking_type, sentence[:150]))
|
|
110
|
+
break # One match per sentence is enough
|
|
111
|
+
|
|
112
|
+
return thinking_segments[:5] # Limit to 5 most important
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def extract_decisions(response_text: str) -> list:
|
|
116
|
+
"""
|
|
117
|
+
Extract decision points from Claude's response.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
list: List of (decision_type, text) tuples
|
|
121
|
+
"""
|
|
122
|
+
decisions = []
|
|
123
|
+
sentences = re.split(r'[.!?]\s+', response_text)
|
|
124
|
+
|
|
125
|
+
for sentence in sentences:
|
|
126
|
+
for pattern, decision_type in DECISION_PATTERNS:
|
|
127
|
+
if re.search(pattern, sentence, re.IGNORECASE):
|
|
128
|
+
decisions.append((decision_type, sentence[:150]))
|
|
129
|
+
break
|
|
130
|
+
|
|
131
|
+
return decisions[:5] # Limit to 5 decisions
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def detect_outcome(response_text: str) -> tuple:
|
|
135
|
+
"""
|
|
136
|
+
Detect outcome status and generate summary from response.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
tuple: (status, summary) where status is 'success', 'failed', or 'partial'
|
|
140
|
+
"""
|
|
141
|
+
# Check for failure first (most specific)
|
|
142
|
+
for pattern in OUTCOME_FAILED_PATTERNS:
|
|
143
|
+
if re.search(pattern, response_text, re.IGNORECASE):
|
|
144
|
+
# Extract a brief summary from around the match
|
|
145
|
+
match = re.search(pattern + r".{0,100}", response_text, re.IGNORECASE)
|
|
146
|
+
summary = match.group(0)[:150] if match else "Request encountered an error"
|
|
147
|
+
return "failed", f"FAILED - {summary}"
|
|
148
|
+
|
|
149
|
+
# Check for partial/pending
|
|
150
|
+
for pattern in OUTCOME_PARTIAL_PATTERNS:
|
|
151
|
+
if re.search(pattern, response_text, re.IGNORECASE):
|
|
152
|
+
# Get first line or first 100 chars as summary
|
|
153
|
+
first_line = response_text.split('\n')[0][:150]
|
|
154
|
+
return "partial", f"PARTIAL - {first_line}"
|
|
155
|
+
|
|
156
|
+
# Check for explicit success
|
|
157
|
+
for pattern in OUTCOME_SUCCESS_PATTERNS:
|
|
158
|
+
if re.search(pattern, response_text, re.IGNORECASE):
|
|
159
|
+
# Get first line as success summary
|
|
160
|
+
first_line = response_text.split('\n')[0][:150]
|
|
161
|
+
return "success", f"SUCCESS - {first_line}"
|
|
162
|
+
|
|
163
|
+
# Default: assume success if response is substantial
|
|
164
|
+
if len(response_text) > 100:
|
|
165
|
+
first_line = response_text.split('\n')[0][:150]
|
|
166
|
+
return "success", f"COMPLETED - {first_line}"
|
|
167
|
+
|
|
168
|
+
return "partial", "Response generated"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def load_session_data():
|
|
172
|
+
"""Load session data from JSON file."""
|
|
173
|
+
session_file = Path(os.getcwd()) / ".claude_session"
|
|
174
|
+
if session_file.exists():
|
|
175
|
+
try:
|
|
176
|
+
content = session_file.read_text().strip()
|
|
177
|
+
# Try JSON format first
|
|
178
|
+
return json.loads(content)
|
|
179
|
+
except json.JSONDecodeError as e:
|
|
180
|
+
logger.debug(f"JSON decode error, trying legacy format: {e}")
|
|
181
|
+
# Fall back to legacy plain text format (just session_id)
|
|
182
|
+
try:
|
|
183
|
+
content = session_file.read_text().strip()
|
|
184
|
+
return {"session_id": content}
|
|
185
|
+
except (IOError, OSError) as read_err:
|
|
186
|
+
logger.warning(f"Failed to read session file: {read_err}")
|
|
187
|
+
return None
|
|
188
|
+
except (IOError, OSError) as e:
|
|
189
|
+
logger.warning(f"Failed to read session file: {e}")
|
|
190
|
+
return None
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def get_session_id():
|
|
195
|
+
"""Get session ID from file."""
|
|
196
|
+
data = load_session_data()
|
|
197
|
+
return data.get("session_id") if data else None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def call_memory_agent(skill_id: str, params: dict) -> dict:
|
|
201
|
+
"""Call the memory agent API."""
|
|
202
|
+
try:
|
|
203
|
+
response = requests.post(
|
|
204
|
+
f"{MEMORY_AGENT_URL}/a2a",
|
|
205
|
+
json={
|
|
206
|
+
"jsonrpc": "2.0",
|
|
207
|
+
"id": "detect-hook",
|
|
208
|
+
"method": "tasks/send",
|
|
209
|
+
"params": {
|
|
210
|
+
"message": {"parts": [{"type": "text", "text": ""}]},
|
|
211
|
+
"metadata": {
|
|
212
|
+
"skill_id": skill_id,
|
|
213
|
+
"params": params
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
timeout=API_TIMEOUT
|
|
218
|
+
)
|
|
219
|
+
return response.json()
|
|
220
|
+
except requests.RequestException as e:
|
|
221
|
+
logger.debug(f"Memory agent request failed for skill '{skill_id}': {e}")
|
|
222
|
+
return None
|
|
223
|
+
except json.JSONDecodeError as e:
|
|
224
|
+
logger.debug(f"Failed to decode memory agent response for skill '{skill_id}': {e}")
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def main():
|
|
229
|
+
"""Analyze Claude's response and log detected events using BATCHED API calls."""
|
|
230
|
+
# Read hook input from stdin
|
|
231
|
+
try:
|
|
232
|
+
hook_input = json.load(sys.stdin)
|
|
233
|
+
except json.JSONDecodeError as e:
|
|
234
|
+
logger.debug(f"Failed to parse hook input JSON: {e}")
|
|
235
|
+
sys.exit(0)
|
|
236
|
+
except (IOError, OSError) as e:
|
|
237
|
+
logger.debug(f"Failed to read stdin: {e}")
|
|
238
|
+
sys.exit(0)
|
|
239
|
+
|
|
240
|
+
# Get Claude's response
|
|
241
|
+
# The Stop hook receives the assistant's message
|
|
242
|
+
response_text = ""
|
|
243
|
+
|
|
244
|
+
# Try different possible formats
|
|
245
|
+
if "assistant_message" in hook_input:
|
|
246
|
+
response_text = hook_input["assistant_message"]
|
|
247
|
+
elif "message" in hook_input:
|
|
248
|
+
msg = hook_input["message"]
|
|
249
|
+
if isinstance(msg, str):
|
|
250
|
+
response_text = msg
|
|
251
|
+
elif isinstance(msg, dict):
|
|
252
|
+
response_text = msg.get("content", "")
|
|
253
|
+
elif "transcript" in hook_input:
|
|
254
|
+
# Get last assistant message from transcript
|
|
255
|
+
transcript = hook_input["transcript"]
|
|
256
|
+
for msg in reversed(transcript):
|
|
257
|
+
if msg.get("role") == "assistant":
|
|
258
|
+
response_text = msg.get("content", "")
|
|
259
|
+
break
|
|
260
|
+
|
|
261
|
+
if not response_text or len(response_text) < 50:
|
|
262
|
+
sys.exit(0)
|
|
263
|
+
|
|
264
|
+
# Load session data (includes current_request_id for causal chain)
|
|
265
|
+
session_data = load_session_data()
|
|
266
|
+
if not session_data:
|
|
267
|
+
sys.exit(0)
|
|
268
|
+
|
|
269
|
+
session_id = session_data.get("session_id")
|
|
270
|
+
if not session_id:
|
|
271
|
+
sys.exit(0)
|
|
272
|
+
|
|
273
|
+
# Get the current request ID for causal chain linking
|
|
274
|
+
root_event_id = session_data.get("current_request_id")
|
|
275
|
+
project_path = os.getcwd()
|
|
276
|
+
|
|
277
|
+
# =====================================================================
|
|
278
|
+
# OPTIMIZATION: Collect ALL events first, then send in ONE batched call
|
|
279
|
+
# This reduces 5+ HTTP calls to just 2 (auto_detect + batch)
|
|
280
|
+
# =====================================================================
|
|
281
|
+
|
|
282
|
+
# Build params for auto-detect (this does LLM-based analysis)
|
|
283
|
+
auto_detect_params = {
|
|
284
|
+
"session_id": session_id,
|
|
285
|
+
"response_text": response_text,
|
|
286
|
+
"project_path": project_path
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
# Add causal chain link if we have a root event
|
|
290
|
+
if root_event_id:
|
|
291
|
+
auto_detect_params["parent_event_id"] = root_event_id
|
|
292
|
+
auto_detect_params["root_event_id"] = root_event_id
|
|
293
|
+
|
|
294
|
+
# Call auto-detect to analyze response for decisions/observations (1 API call)
|
|
295
|
+
call_memory_agent("timeline_auto_detect", auto_detect_params)
|
|
296
|
+
|
|
297
|
+
# =====================================================================
|
|
298
|
+
# Collect all additional events into a single batch
|
|
299
|
+
# =====================================================================
|
|
300
|
+
batch_events = []
|
|
301
|
+
|
|
302
|
+
# Extract thinking segments (the REASONING)
|
|
303
|
+
thinking_segments = extract_thinking(response_text)
|
|
304
|
+
for thinking_type, thinking_text in thinking_segments:
|
|
305
|
+
batch_events.append({
|
|
306
|
+
"event_type": "thinking",
|
|
307
|
+
"summary": f"[{thinking_type}] {thinking_text}"[:200],
|
|
308
|
+
"confidence": 0.6 # Lower confidence for regex-detected thinking
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
# Extract decision points (the CHOICES)
|
|
312
|
+
decisions = extract_decisions(response_text)
|
|
313
|
+
for decision_type, decision_text in decisions:
|
|
314
|
+
batch_events.append({
|
|
315
|
+
"event_type": "decision",
|
|
316
|
+
"summary": f"[{decision_type}] {decision_text}"[:200],
|
|
317
|
+
"confidence": 0.7 # Moderate confidence for regex-detected decisions
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
# Add outcome event summarizing the result
|
|
321
|
+
if root_event_id:
|
|
322
|
+
status, summary = detect_outcome(response_text)
|
|
323
|
+
batch_events.append({
|
|
324
|
+
"event_type": "outcome",
|
|
325
|
+
"summary": summary[:200],
|
|
326
|
+
"status": status
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
# =====================================================================
|
|
330
|
+
# Send all events in ONE batched API call (instead of 5+ separate calls)
|
|
331
|
+
# =====================================================================
|
|
332
|
+
if batch_events:
|
|
333
|
+
batch_params = {
|
|
334
|
+
"session_id": session_id,
|
|
335
|
+
"events": batch_events,
|
|
336
|
+
"project_path": project_path
|
|
337
|
+
}
|
|
338
|
+
if root_event_id:
|
|
339
|
+
batch_params["root_event_id"] = root_event_id
|
|
340
|
+
batch_params["parent_event_id"] = root_event_id
|
|
341
|
+
|
|
342
|
+
call_memory_agent("timeline_log_batch", batch_params)
|
|
343
|
+
|
|
344
|
+
sys.exit(0)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
if __name__ == "__main__":
|
|
348
|
+
main()
|