claude-memory-agent 3.1.0 → 3.2.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/bin/lib/installer.js +3 -1
- package/bin/lib/steps/advanced.js +1 -1
- package/config.py +10 -2
- package/hooks/extract_memories.py +104 -0
- package/hooks/grounding-hook-v2.py +169 -33
- package/hooks/pre_compact_hook.py +269 -5
- package/hooks/stop_hook.py +191 -13
- package/install.py +2 -2
- package/main.py +389 -0
- package/mcp_proxy.py +87 -6
- package/package.json +2 -2
- package/services/database.py +267 -1
- package/services/embeddings.py +1 -1
- package/services/retry_queue.py +5 -1
- package/services/soul.py +415 -91
- package/services/vector_index.py +5 -1
- package/update_system.py +34 -8
package/bin/lib/installer.js
CHANGED
|
@@ -58,7 +58,9 @@ function runSync(cmd, cwd) {
|
|
|
58
58
|
*/
|
|
59
59
|
function buildEnvContent(config, agentDir) {
|
|
60
60
|
const timestamp = new Date().toISOString();
|
|
61
|
-
const
|
|
61
|
+
const homedir = require('os').homedir();
|
|
62
|
+
const defaultDbPath = path.join(homedir, '.claude-memory', 'memories.db').replace(/\\/g, '/');
|
|
63
|
+
const dbPath = config.dbPath || defaultDbPath;
|
|
62
64
|
const memoryUrl = 'http://' + (config.host === '0.0.0.0' ? 'localhost' : config.host) + ':' + config.port;
|
|
63
65
|
|
|
64
66
|
const lines = [
|
package/config.py
CHANGED
|
@@ -21,6 +21,9 @@ logger = logging.getLogger(__name__)
|
|
|
21
21
|
AGENT_DIR = Path(__file__).parent.resolve()
|
|
22
22
|
load_dotenv(AGENT_DIR / ".env")
|
|
23
23
|
|
|
24
|
+
# User data directory — safe from code updates (zip, git pull, npm update)
|
|
25
|
+
USER_DATA_DIR = Path(os.getenv("USER_DATA_DIR", str(Path.home() / ".claude-memory")))
|
|
26
|
+
|
|
24
27
|
|
|
25
28
|
class Config:
|
|
26
29
|
"""Configuration singleton with environment variable loading."""
|
|
@@ -30,11 +33,15 @@ class Config:
|
|
|
30
33
|
self.AGENT_DIR = AGENT_DIR
|
|
31
34
|
self.DATABASE_PATH = Path(os.getenv(
|
|
32
35
|
"DATABASE_PATH",
|
|
33
|
-
str(
|
|
36
|
+
str(USER_DATA_DIR / "memories.db")
|
|
34
37
|
))
|
|
35
38
|
self.INDEX_DIR = Path(os.getenv(
|
|
36
39
|
"INDEX_DIR",
|
|
37
|
-
str(
|
|
40
|
+
str(USER_DATA_DIR / "indexes")
|
|
41
|
+
))
|
|
42
|
+
self.QUEUE_DB_PATH = Path(os.getenv(
|
|
43
|
+
"QUEUE_DB_PATH",
|
|
44
|
+
str(USER_DATA_DIR / "queue.db")
|
|
38
45
|
))
|
|
39
46
|
self.LOG_FILE = AGENT_DIR / "memory-agent.log"
|
|
40
47
|
self.LOCK_FILE = AGENT_DIR / "memory-agent.lock"
|
|
@@ -207,3 +214,4 @@ OLLAMA_HOST = config.OLLAMA_HOST
|
|
|
207
214
|
EMBEDDING_PROVIDER = config.EMBEDDING_PROVIDER
|
|
208
215
|
EMBEDDING_MODEL = config.EMBEDDING_MODEL
|
|
209
216
|
DATABASE_PATH = config.DATABASE_PATH
|
|
217
|
+
QUEUE_DB_PATH = config.QUEUE_DB_PATH
|
|
@@ -66,10 +66,23 @@ PATTERN_PATTERNS = [
|
|
|
66
66
|
re.compile(r"(?:^|\n)\s*(?:Always|Never|Should always|Should never|Must always|Must never) (.*?)(?:\.|$)", re.IGNORECASE | re.MULTILINE),
|
|
67
67
|
]
|
|
68
68
|
|
|
69
|
+
# Workflow/procedure patterns
|
|
70
|
+
WORKFLOW_PATTERNS = [
|
|
71
|
+
# "To build X, run Y" / "To deploy X, do Y"
|
|
72
|
+
re.compile(r"(?:^|\n)\s*(?:To|to) (\w[\w\s]{3,30}),\s*(?:run|do|use|execute|type) (.{10,}?)(?:\n|$)", re.IGNORECASE | re.MULTILINE),
|
|
73
|
+
# "learned how to..."
|
|
74
|
+
re.compile(r"(?:^|\n)\s*(?:I learned|We learned|learned how to|figured out how to) (.{20,}?)(?:\.|$)", re.IGNORECASE | re.MULTILINE),
|
|
75
|
+
# Step-by-step: "1. ...\n2. ...\n3. ..."
|
|
76
|
+
re.compile(r"(?:^|\n)\s*1[.)]\s+(.+)\n\s*2[.)]\s+(.+)\n\s*3[.)]\s+(.+)", re.MULTILINE),
|
|
77
|
+
# "The workflow is..." / "The process is..."
|
|
78
|
+
re.compile(r"(?:^|\n)\s*(?:The workflow|The process|The procedure|Steps to) (?:is|are|for)[:\s]+(.*?)(?:\n\n|\Z)", re.IGNORECASE | re.DOTALL),
|
|
79
|
+
]
|
|
80
|
+
|
|
69
81
|
# Broader keyword triggers (used for line-level scanning)
|
|
70
82
|
DECISION_KEYWORDS = {"decided", "let's use", "going with", "chose", "choosing", "will use", "the plan is", "approach is", "strategy is", "i'll implement", "we'll implement"}
|
|
71
83
|
ERROR_KEYWORDS = {"error", "bug", "fix", "issue", "traceback", "exception", "failed", "failure", "broken", "crash", "root cause"}
|
|
72
84
|
PATTERN_KEYWORDS = {"pattern", "approach", "architecture", "convention", "best practice", "always", "never", "rule"}
|
|
85
|
+
WORKFLOW_KEYWORDS = {"workflow", "procedure", "steps to", "how to", "process for", "pipeline", "build steps", "deploy steps"}
|
|
73
86
|
|
|
74
87
|
|
|
75
88
|
# ---------------------------------------------------------------------------
|
|
@@ -224,6 +237,13 @@ def extract_from_text(text: str, existing_hashes: set) -> List[Dict[str, Any]]:
|
|
|
224
237
|
if len(context) > 30:
|
|
225
238
|
add_extraction(context, "code", 6, ["pattern"])
|
|
226
239
|
|
|
240
|
+
# Workflows / Procedures
|
|
241
|
+
for pattern in WORKFLOW_PATTERNS:
|
|
242
|
+
for match in pattern.finditer(text):
|
|
243
|
+
context = extract_context_around(text, match.start(), match.end(), context_chars=300)
|
|
244
|
+
if len(context) > 40:
|
|
245
|
+
add_extraction(context, "code", 7, ["workflow", "procedure"])
|
|
246
|
+
|
|
227
247
|
# --- Line-level keyword scanning (fallback for cases regex misses) ---
|
|
228
248
|
# Only do this if we have not yet hit our cap
|
|
229
249
|
if len(extractions) < MAX_MEMORIES_PER_RUN:
|
|
@@ -256,6 +276,12 @@ def extract_from_text(text: str, existing_hashes: set) -> List[Dict[str, Any]]:
|
|
|
256
276
|
if len(block) > 30:
|
|
257
277
|
add_extraction(block, "code", 5, ["pattern", "keyword-match"])
|
|
258
278
|
|
|
279
|
+
# Check for workflow keywords
|
|
280
|
+
elif any(kw in line_lower for kw in WORKFLOW_KEYWORDS):
|
|
281
|
+
block = '\n'.join(lines[i:i+5]).strip() # Wider context for workflows
|
|
282
|
+
if len(block) > 40:
|
|
283
|
+
add_extraction(block, "code", 6, ["workflow", "keyword-match"])
|
|
284
|
+
|
|
259
285
|
i += 1
|
|
260
286
|
|
|
261
287
|
return extractions
|
|
@@ -313,6 +339,84 @@ def store_memory_sync(extraction: Dict[str, Any], project_path: Optional[str] =
|
|
|
313
339
|
return False
|
|
314
340
|
|
|
315
341
|
|
|
342
|
+
# ---------------------------------------------------------------------------
|
|
343
|
+
# Workflow / Bash command extraction from JSONL transcript
|
|
344
|
+
# ---------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
def extract_bash_commands(transcript_path: str, byte_offset: int = 0) -> List[str]:
|
|
347
|
+
"""Extract successful bash commands from JSONL transcript.
|
|
348
|
+
|
|
349
|
+
Looks for tool_use blocks with tool=Bash that were followed by success results.
|
|
350
|
+
Returns deduplicated command list.
|
|
351
|
+
"""
|
|
352
|
+
path = Path(transcript_path)
|
|
353
|
+
if not path.exists():
|
|
354
|
+
return []
|
|
355
|
+
|
|
356
|
+
commands = []
|
|
357
|
+
seen = set()
|
|
358
|
+
try:
|
|
359
|
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
360
|
+
if byte_offset > 0:
|
|
361
|
+
f.seek(byte_offset)
|
|
362
|
+
f.readline() # skip partial line
|
|
363
|
+
for line in f:
|
|
364
|
+
line = line.strip()
|
|
365
|
+
if not line:
|
|
366
|
+
continue
|
|
367
|
+
try:
|
|
368
|
+
msg = json.loads(line)
|
|
369
|
+
content = msg.get("content", [])
|
|
370
|
+
if not isinstance(content, list):
|
|
371
|
+
continue
|
|
372
|
+
for part in content:
|
|
373
|
+
if not isinstance(part, dict):
|
|
374
|
+
continue
|
|
375
|
+
if part.get("type") == "tool_use" and part.get("name") == "Bash":
|
|
376
|
+
cmd = ""
|
|
377
|
+
inp = part.get("input", {})
|
|
378
|
+
if isinstance(inp, dict):
|
|
379
|
+
cmd = inp.get("command", "")
|
|
380
|
+
if cmd and len(cmd) > 5 and cmd not in seen:
|
|
381
|
+
# Skip trivial commands
|
|
382
|
+
if not cmd.strip().startswith(("ls", "pwd", "echo", "cat ")):
|
|
383
|
+
seen.add(cmd)
|
|
384
|
+
commands.append(cmd)
|
|
385
|
+
except (json.JSONDecodeError, TypeError):
|
|
386
|
+
continue
|
|
387
|
+
except OSError:
|
|
388
|
+
pass
|
|
389
|
+
return commands[-20:] # Keep last 20 commands
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def capture_workflow_sync(name: str, steps: List[str], commands: List[str],
|
|
393
|
+
project_path: Optional[str] = None) -> bool:
|
|
394
|
+
"""POST a captured workflow to /api/workflow/capture."""
|
|
395
|
+
import urllib.request
|
|
396
|
+
import urllib.error
|
|
397
|
+
|
|
398
|
+
payload = json.dumps({
|
|
399
|
+
"name": name,
|
|
400
|
+
"steps": steps,
|
|
401
|
+
"commands": commands,
|
|
402
|
+
"project_path": project_path or "",
|
|
403
|
+
}).encode("utf-8")
|
|
404
|
+
|
|
405
|
+
headers = {"Content-Type": "application/json"}
|
|
406
|
+
if API_KEY:
|
|
407
|
+
headers["X-Memory-Key"] = API_KEY
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
req = urllib.request.Request(
|
|
411
|
+
f"{MEMORY_AGENT_URL}/api/workflow/capture",
|
|
412
|
+
data=payload, headers=headers, method="POST",
|
|
413
|
+
)
|
|
414
|
+
with urllib.request.urlopen(req, timeout=2) as resp:
|
|
415
|
+
return resp.status == 200
|
|
416
|
+
except (urllib.error.URLError, urllib.error.HTTPError, OSError, TimeoutError):
|
|
417
|
+
return False
|
|
418
|
+
|
|
419
|
+
|
|
316
420
|
# ---------------------------------------------------------------------------
|
|
317
421
|
# Main entry point
|
|
318
422
|
# ---------------------------------------------------------------------------
|
|
@@ -7,14 +7,23 @@ server-side.
|
|
|
7
7
|
|
|
8
8
|
Also replaces: session_start.py, problem-detector.py, memory-first-reminder.py
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
Fresh session detection:
|
|
11
|
+
- Tracks last grounded session_id via .claude_session_meta
|
|
12
|
+
- Fresh session -> calls /api/grounding-context/rich (~500-800 tokens)
|
|
13
|
+
- Continuing session -> calls /api/grounding-context (~150 tokens)
|
|
14
|
+
|
|
15
|
+
Design constraints:
|
|
16
|
+
- Uses stdlib only (no pip dependencies) -- urllib.request, not requests
|
|
17
|
+
- Timeout: 3 seconds, silent fail
|
|
18
|
+
- Always exits 0 -- never blocks Claude Code
|
|
12
19
|
"""
|
|
13
20
|
|
|
14
21
|
import os
|
|
15
22
|
import sys
|
|
16
23
|
import json
|
|
17
24
|
import logging
|
|
25
|
+
import urllib.request
|
|
26
|
+
import urllib.error
|
|
18
27
|
from pathlib import Path
|
|
19
28
|
|
|
20
29
|
logging.basicConfig(
|
|
@@ -26,15 +35,61 @@ logger = logging.getLogger("grounding-v2")
|
|
|
26
35
|
|
|
27
36
|
MEMORY_AGENT_URL = os.getenv("MEMORY_AGENT_URL", "http://localhost:8102")
|
|
28
37
|
TIMEOUT = 3 # seconds
|
|
38
|
+
SESSION_META_DIR = Path.home() / ".claude"
|
|
39
|
+
SESSION_META_FILE = SESSION_META_DIR / ".claude_session_meta"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# HTTP helper (stdlib only -- no requests dependency)
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
def _http_post(url: str, payload: dict, timeout: float = TIMEOUT):
|
|
47
|
+
"""POST JSON to url and return parsed response dict, or None on failure."""
|
|
48
|
+
data = json.dumps(payload).encode("utf-8")
|
|
49
|
+
req = urllib.request.Request(
|
|
50
|
+
url, data=data,
|
|
51
|
+
headers={"Content-Type": "application/json"},
|
|
52
|
+
method="POST",
|
|
53
|
+
)
|
|
54
|
+
try:
|
|
55
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
56
|
+
if resp.status == 200:
|
|
57
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
58
|
+
except (urllib.error.URLError, urllib.error.HTTPError, OSError, TimeoutError):
|
|
59
|
+
pass
|
|
60
|
+
return None
|
|
61
|
+
|
|
29
62
|
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Stdin / session ID helpers
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
30
66
|
|
|
31
|
-
def
|
|
32
|
-
"""
|
|
67
|
+
def read_stdin_payload() -> dict:
|
|
68
|
+
"""Read the full JSON payload from Claude Code stdin (once)."""
|
|
69
|
+
try:
|
|
70
|
+
if not sys.stdin.isatty():
|
|
71
|
+
data = sys.stdin.read()
|
|
72
|
+
if data:
|
|
73
|
+
return json.loads(data)
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
return {}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_session_id(payload: dict, project_path: str) -> str:
|
|
80
|
+
"""Get session ID from stdin payload, env, or .claude_session file."""
|
|
81
|
+
# 1. stdin payload (Claude Code's actual format)
|
|
82
|
+
sid = payload.get("session_id", "")
|
|
83
|
+
if sid:
|
|
84
|
+
return sid
|
|
85
|
+
|
|
86
|
+
# 2. env var
|
|
33
87
|
sid = os.getenv("CLAUDE_SESSION_ID", "")
|
|
34
88
|
if sid:
|
|
35
89
|
return sid
|
|
36
90
|
|
|
37
|
-
|
|
91
|
+
# 3. .claude_session file
|
|
92
|
+
session_file = Path(project_path) / ".claude_session"
|
|
38
93
|
if session_file.exists():
|
|
39
94
|
try:
|
|
40
95
|
content = session_file.read_text().strip()
|
|
@@ -45,31 +100,87 @@ def get_session_id() -> str:
|
|
|
45
100
|
return ""
|
|
46
101
|
|
|
47
102
|
|
|
48
|
-
|
|
49
|
-
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# Fresh session detection
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
def is_fresh_session(session_id: str, project_path: str) -> bool:
|
|
108
|
+
"""Detect if this is a fresh/resumed session by comparing to last grounded session_id.
|
|
109
|
+
|
|
110
|
+
Uses .claude_session_meta to track what we last grounded for this project.
|
|
111
|
+
Returns True if session_id differs from last grounded (= new session or context cleared).
|
|
112
|
+
"""
|
|
50
113
|
try:
|
|
51
|
-
if
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
114
|
+
if SESSION_META_FILE.exists():
|
|
115
|
+
meta = json.loads(SESSION_META_FILE.read_text(encoding="utf-8"))
|
|
116
|
+
last_grounded = meta.get(project_path, {}).get("last_grounded_session", "")
|
|
117
|
+
last_grounded_count = meta.get(project_path, {}).get("grounding_count", 0)
|
|
118
|
+
if last_grounded == session_id and last_grounded_count > 0:
|
|
119
|
+
return False
|
|
120
|
+
return True
|
|
121
|
+
except (json.JSONDecodeError, OSError):
|
|
57
122
|
pass
|
|
58
|
-
return
|
|
123
|
+
return True # Default to fresh if can't read meta
|
|
59
124
|
|
|
60
125
|
|
|
126
|
+
def update_session_meta(session_id: str, project_path: str):
|
|
127
|
+
"""Record that we've grounded this session_id for this project."""
|
|
128
|
+
try:
|
|
129
|
+
SESSION_META_DIR.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
meta = {}
|
|
131
|
+
if SESSION_META_FILE.exists():
|
|
132
|
+
try:
|
|
133
|
+
meta = json.loads(SESSION_META_FILE.read_text(encoding="utf-8"))
|
|
134
|
+
except (json.JSONDecodeError, OSError):
|
|
135
|
+
meta = {}
|
|
136
|
+
|
|
137
|
+
if project_path not in meta:
|
|
138
|
+
meta[project_path] = {}
|
|
139
|
+
|
|
140
|
+
current = meta[project_path]
|
|
141
|
+
if current.get("last_grounded_session") == session_id:
|
|
142
|
+
current["grounding_count"] = current.get("grounding_count", 0) + 1
|
|
143
|
+
else:
|
|
144
|
+
current["last_grounded_session"] = session_id
|
|
145
|
+
current["grounding_count"] = 1
|
|
146
|
+
|
|
147
|
+
# Prune old projects (keep 20)
|
|
148
|
+
if len(meta) > 20:
|
|
149
|
+
for old_key in sorted(meta.keys())[:-20]:
|
|
150
|
+
del meta[old_key]
|
|
151
|
+
|
|
152
|
+
SESSION_META_FILE.write_text(json.dumps(meta, indent=2), encoding="utf-8")
|
|
153
|
+
except OSError:
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# Self-reflection hint (appended to every grounding context)
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
LEARNING_HINT = (
|
|
162
|
+
"\nIf you discovered something new/unique this response "
|
|
163
|
+
"(a non-obvious fix, a codebase quirk, a useful workflow), "
|
|
164
|
+
"append a hidden tag at the very end: <!-- LEARNED: one-sentence description -->"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
# Main
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
|
|
61
172
|
def main():
|
|
62
|
-
|
|
63
|
-
project_path = os.getcwd()
|
|
64
|
-
user_input =
|
|
173
|
+
payload = read_stdin_payload()
|
|
174
|
+
project_path = payload.get("cwd", "") or os.getcwd()
|
|
175
|
+
user_input = payload.get("prompt", "") or payload.get("user_prompt", "")
|
|
176
|
+
session_id = get_session_id(payload, project_path)
|
|
65
177
|
|
|
66
178
|
if not session_id:
|
|
67
179
|
# No session - try to initialize one via A2A
|
|
68
180
|
try:
|
|
69
|
-
|
|
70
|
-
resp = requests.post(
|
|
181
|
+
result = _http_post(
|
|
71
182
|
f"{MEMORY_AGENT_URL}/a2a",
|
|
72
|
-
|
|
183
|
+
{
|
|
73
184
|
"jsonrpc": "2.0",
|
|
74
185
|
"id": "grounding-v2-init",
|
|
75
186
|
"method": "tasks/send",
|
|
@@ -81,16 +192,13 @@ def main():
|
|
|
81
192
|
},
|
|
82
193
|
},
|
|
83
194
|
},
|
|
84
|
-
timeout=TIMEOUT,
|
|
85
195
|
)
|
|
86
|
-
if
|
|
87
|
-
result = resp.json()
|
|
196
|
+
if result:
|
|
88
197
|
try:
|
|
89
198
|
text = result["result"]["artifacts"][0]["parts"][0]["text"]
|
|
90
199
|
data = json.loads(text)
|
|
91
200
|
session_id = data.get("session_id", "")
|
|
92
201
|
if session_id:
|
|
93
|
-
# Save for future hooks
|
|
94
202
|
sf = Path(project_path) / ".claude_session"
|
|
95
203
|
sf.write_text(json.dumps({"session_id": session_id}))
|
|
96
204
|
except (KeyError, IndexError, json.JSONDecodeError):
|
|
@@ -101,26 +209,54 @@ def main():
|
|
|
101
209
|
if not session_id:
|
|
102
210
|
sys.exit(0)
|
|
103
211
|
|
|
104
|
-
#
|
|
212
|
+
# Register session as active (for cross-session awareness)
|
|
213
|
+
try:
|
|
214
|
+
_http_post(
|
|
215
|
+
f"{MEMORY_AGENT_URL}/api/sessions/register",
|
|
216
|
+
{"session_id": session_id, "project_path": project_path},
|
|
217
|
+
)
|
|
218
|
+
except Exception as e:
|
|
219
|
+
logger.debug(f"Session register failed: {e}")
|
|
220
|
+
|
|
221
|
+
# Detect fresh vs continuing session
|
|
222
|
+
fresh = is_fresh_session(session_id, project_path)
|
|
223
|
+
|
|
224
|
+
if fresh:
|
|
225
|
+
# Fresh session: use rich grounding context (~500-800 tokens)
|
|
226
|
+
try:
|
|
227
|
+
data = _http_post(
|
|
228
|
+
f"{MEMORY_AGENT_URL}/api/grounding-context/rich",
|
|
229
|
+
{"session_id": session_id, "project_path": project_path},
|
|
230
|
+
)
|
|
231
|
+
if data:
|
|
232
|
+
context = data.get("context", "")
|
|
233
|
+
if context:
|
|
234
|
+
print(context + LEARNING_HINT)
|
|
235
|
+
update_session_meta(session_id, project_path)
|
|
236
|
+
sys.exit(0)
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.debug(f"Rich grounding context call failed: {e}")
|
|
239
|
+
# Fall through to slim context
|
|
240
|
+
|
|
241
|
+
# Continuing session (or rich context failed): use slim grounding context (~150 tokens)
|
|
105
242
|
try:
|
|
106
|
-
|
|
107
|
-
resp = requests.post(
|
|
243
|
+
data = _http_post(
|
|
108
244
|
f"{MEMORY_AGENT_URL}/api/grounding-context",
|
|
109
|
-
|
|
245
|
+
{
|
|
110
246
|
"session_id": session_id,
|
|
111
247
|
"project_path": project_path,
|
|
112
248
|
"user_input": user_input,
|
|
113
249
|
},
|
|
114
|
-
timeout=TIMEOUT,
|
|
115
250
|
)
|
|
116
|
-
if
|
|
117
|
-
data = resp.json()
|
|
251
|
+
if data:
|
|
118
252
|
context = data.get("context", "")
|
|
119
253
|
if context:
|
|
120
|
-
print(context)
|
|
254
|
+
print(context + LEARNING_HINT)
|
|
121
255
|
except Exception as e:
|
|
122
256
|
logger.debug(f"Grounding context call failed: {e}")
|
|
123
|
-
|
|
257
|
+
|
|
258
|
+
# Update meta (even for slim context, so we track this session)
|
|
259
|
+
update_session_meta(session_id, project_path)
|
|
124
260
|
|
|
125
261
|
sys.exit(0)
|
|
126
262
|
|