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.
Files changed (100) hide show
  1. package/.env.example +107 -0
  2. package/README.md +200 -0
  3. package/agent_card.py +512 -0
  4. package/bin/cli.js +181 -0
  5. package/bin/postinstall.js +216 -0
  6. package/config.py +104 -0
  7. package/dashboard.html +2689 -0
  8. package/hooks/README.md +196 -0
  9. package/hooks/__pycache__/auto-detect-response.cpython-312.pyc +0 -0
  10. package/hooks/__pycache__/auto_capture.cpython-312.pyc +0 -0
  11. package/hooks/__pycache__/session_end.cpython-312.pyc +0 -0
  12. package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
  13. package/hooks/auto-detect-response.py +348 -0
  14. package/hooks/auto_capture.py +255 -0
  15. package/hooks/detect-correction.py +173 -0
  16. package/hooks/grounding-hook.py +348 -0
  17. package/hooks/log-tool-use.py +234 -0
  18. package/hooks/log-user-request.py +208 -0
  19. package/hooks/pre-tool-decision.py +218 -0
  20. package/hooks/problem-detector.py +343 -0
  21. package/hooks/session_end.py +192 -0
  22. package/hooks/session_start.py +227 -0
  23. package/install.py +887 -0
  24. package/main.py +2859 -0
  25. package/manager.py +997 -0
  26. package/package.json +55 -0
  27. package/requirements.txt +8 -0
  28. package/run_server.py +136 -0
  29. package/services/__init__.py +50 -0
  30. package/services/__pycache__/__init__.cpython-312.pyc +0 -0
  31. package/services/__pycache__/agent_registry.cpython-312.pyc +0 -0
  32. package/services/__pycache__/auth.cpython-312.pyc +0 -0
  33. package/services/__pycache__/auto_inject.cpython-312.pyc +0 -0
  34. package/services/__pycache__/claude_md_sync.cpython-312.pyc +0 -0
  35. package/services/__pycache__/cleanup.cpython-312.pyc +0 -0
  36. package/services/__pycache__/compaction_flush.cpython-312.pyc +0 -0
  37. package/services/__pycache__/confidence.cpython-312.pyc +0 -0
  38. package/services/__pycache__/daily_log.cpython-312.pyc +0 -0
  39. package/services/__pycache__/database.cpython-312.pyc +0 -0
  40. package/services/__pycache__/embeddings.cpython-312.pyc +0 -0
  41. package/services/__pycache__/insights.cpython-312.pyc +0 -0
  42. package/services/__pycache__/llm_analyzer.cpython-312.pyc +0 -0
  43. package/services/__pycache__/memory_md_sync.cpython-312.pyc +0 -0
  44. package/services/__pycache__/retry_queue.cpython-312.pyc +0 -0
  45. package/services/__pycache__/timeline.cpython-312.pyc +0 -0
  46. package/services/__pycache__/vector_index.cpython-312.pyc +0 -0
  47. package/services/__pycache__/websocket.cpython-312.pyc +0 -0
  48. package/services/agent_registry.py +753 -0
  49. package/services/auth.py +331 -0
  50. package/services/auto_inject.py +250 -0
  51. package/services/claude_md_sync.py +275 -0
  52. package/services/cleanup.py +667 -0
  53. package/services/compaction_flush.py +447 -0
  54. package/services/confidence.py +301 -0
  55. package/services/daily_log.py +333 -0
  56. package/services/database.py +2485 -0
  57. package/services/embeddings.py +358 -0
  58. package/services/insights.py +632 -0
  59. package/services/llm_analyzer.py +595 -0
  60. package/services/memory_md_sync.py +409 -0
  61. package/services/retry_queue.py +453 -0
  62. package/services/timeline.py +579 -0
  63. package/services/vector_index.py +398 -0
  64. package/services/websocket.py +257 -0
  65. package/skills/__init__.py +6 -0
  66. package/skills/__pycache__/__init__.cpython-312.pyc +0 -0
  67. package/skills/__pycache__/admin.cpython-312.pyc +0 -0
  68. package/skills/__pycache__/checkpoint.cpython-312.pyc +0 -0
  69. package/skills/__pycache__/claude_md.cpython-312.pyc +0 -0
  70. package/skills/__pycache__/cleanup.cpython-312.pyc +0 -0
  71. package/skills/__pycache__/grounding.cpython-312.pyc +0 -0
  72. package/skills/__pycache__/insights.cpython-312.pyc +0 -0
  73. package/skills/__pycache__/natural_language.cpython-312.pyc +0 -0
  74. package/skills/__pycache__/retrieve.cpython-312.pyc +0 -0
  75. package/skills/__pycache__/search.cpython-312.pyc +0 -0
  76. package/skills/__pycache__/state.cpython-312.pyc +0 -0
  77. package/skills/__pycache__/store.cpython-312.pyc +0 -0
  78. package/skills/__pycache__/summarize.cpython-312.pyc +0 -0
  79. package/skills/__pycache__/timeline.cpython-312.pyc +0 -0
  80. package/skills/__pycache__/verification.cpython-312.pyc +0 -0
  81. package/skills/admin.py +469 -0
  82. package/skills/checkpoint.py +198 -0
  83. package/skills/claude_md.py +363 -0
  84. package/skills/cleanup.py +241 -0
  85. package/skills/grounding.py +801 -0
  86. package/skills/insights.py +231 -0
  87. package/skills/natural_language.py +277 -0
  88. package/skills/retrieve.py +67 -0
  89. package/skills/search.py +213 -0
  90. package/skills/state.py +182 -0
  91. package/skills/store.py +179 -0
  92. package/skills/summarize.py +588 -0
  93. package/skills/timeline.py +387 -0
  94. package/skills/verification.py +391 -0
  95. package/start_daemon.py +155 -0
  96. package/test_automation.py +221 -0
  97. package/test_complete.py +338 -0
  98. package/test_full.py +322 -0
  99. package/update_system.py +817 -0
  100. package/verify_db.py +134 -0
@@ -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.
@@ -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()