claude-memory-agent 3.0.2 → 3.1.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/dashboard.html +88 -38
- package/hooks/auto-detect-response.py +3 -6
- package/hooks/detect-correction.py +8 -7
- package/hooks/grounding-hook.py +19 -3
- package/hooks/log-tool-use.py +3 -6
- package/hooks/log-user-request.py +14 -6
- package/hooks/pre-tool-decision.py +3 -6
- package/hooks/session_end_hook.py +37 -0
- package/hooks/session_start.py +10 -0
- package/hooks/stop_hook.py +123 -0
- package/install.py +139 -44
- package/main.py +133 -13
- package/mcp_proxy.py +6 -0
- package/package.json +2 -2
- package/services/agent_registry.py +260 -12
- package/services/database.py +188 -1
- package/services/soul.py +467 -0
package/dashboard.html
CHANGED
|
@@ -466,6 +466,31 @@
|
|
|
466
466
|
top: 0; left: 0; right: 0;
|
|
467
467
|
height: 3px;
|
|
468
468
|
}
|
|
469
|
+
.agent-badges {
|
|
470
|
+
display: flex;
|
|
471
|
+
flex-wrap: wrap;
|
|
472
|
+
gap: 6px;
|
|
473
|
+
margin-bottom: 12px;
|
|
474
|
+
}
|
|
475
|
+
.agent-badge {
|
|
476
|
+
padding: 3px 10px;
|
|
477
|
+
border-radius: 12px;
|
|
478
|
+
font-size: 11px;
|
|
479
|
+
font-weight: 600;
|
|
480
|
+
letter-spacing: 0.3px;
|
|
481
|
+
}
|
|
482
|
+
.agent-badge.scope-global {
|
|
483
|
+
background: rgba(88, 166, 255, 0.15);
|
|
484
|
+
color: #58a6ff;
|
|
485
|
+
}
|
|
486
|
+
.agent-badge.scope-project {
|
|
487
|
+
background: rgba(63, 185, 80, 0.15);
|
|
488
|
+
color: #3fb950;
|
|
489
|
+
}
|
|
490
|
+
.agent-badge.category-badge {
|
|
491
|
+
background: var(--bg-tertiary);
|
|
492
|
+
color: var(--text-secondary);
|
|
493
|
+
}
|
|
469
494
|
/* Config Cards */
|
|
470
495
|
.config-card {
|
|
471
496
|
background: var(--bg-card);
|
|
@@ -2205,6 +2230,10 @@
|
|
|
2205
2230
|
|
|
2206
2231
|
<!-- Agents Tab -->
|
|
2207
2232
|
<div class="tab-content active" id="agents-tab">
|
|
2233
|
+
<div style="background: rgba(88,166,255,0.08); border: 1px solid rgba(88,166,255,0.2); border-radius: 12px; padding: 12px 20px; margin-bottom: 20px; display: flex; align-items: center; gap: 12px; font-size: 13px; color: var(--text-secondary);">
|
|
2234
|
+
<i class="fas fa-info-circle" style="color: #58a6ff; font-size: 16px;"></i>
|
|
2235
|
+
<span>Agents discovered from <code style="background: var(--bg-tertiary); padding: 2px 6px; border-radius: 4px;">~/.claude/agents/</code>. Toggle moves files between <code style="background: var(--bg-tertiary); padding: 2px 6px; border-radius: 4px;">agents/</code> and <code style="background: var(--bg-tertiary); padding: 2px 6px; border-radius: 4px;">agents/_disabled/</code>. Global agents are available across all projects.</span>
|
|
2236
|
+
</div>
|
|
2208
2237
|
<div class="section-header">
|
|
2209
2238
|
<div class="search-bar">
|
|
2210
2239
|
<i class="fas fa-search"></i>
|
|
@@ -3056,8 +3085,9 @@
|
|
|
3056
3085
|
|
|
3057
3086
|
async function loadAgentData() {
|
|
3058
3087
|
try {
|
|
3088
|
+
const agentParams = currentProject ? `?project_path=${encodeURIComponent(currentProject)}` : '';
|
|
3059
3089
|
const [agentsRes, mcpsRes, hooksRes] = await Promise.all([
|
|
3060
|
-
fetch(`${API_URL}/api/agents`),
|
|
3090
|
+
fetch(`${API_URL}/api/agents${agentParams}`),
|
|
3061
3091
|
fetch(`${API_URL}/api/mcps`),
|
|
3062
3092
|
fetch(`${API_URL}/api/hooks`)
|
|
3063
3093
|
]);
|
|
@@ -3071,12 +3101,14 @@
|
|
|
3071
3101
|
document.getElementById('agentsTabCount').textContent = allAgents.length;
|
|
3072
3102
|
document.getElementById('mcpsTabCount').textContent = allMcps.length;
|
|
3073
3103
|
document.getElementById('hooksTabCount').textContent = allHooks.length;
|
|
3104
|
+
// Update agent stats from discovery
|
|
3105
|
+
const enabledCount = allAgents.filter(a => a.enabled).length;
|
|
3106
|
+
document.getElementById('enabledAgentsCount').textContent = enabledCount;
|
|
3107
|
+
document.getElementById('agentsTotalCount').textContent = `/ ${allAgents.length} total`;
|
|
3074
3108
|
renderCategoryFilter();
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
renderHooks();
|
|
3079
|
-
}
|
|
3109
|
+
renderAgents();
|
|
3110
|
+
renderMcps();
|
|
3111
|
+
renderHooks();
|
|
3080
3112
|
} catch (e) {
|
|
3081
3113
|
console.error('Error loading agent data:', e);
|
|
3082
3114
|
}
|
|
@@ -3107,21 +3139,26 @@
|
|
|
3107
3139
|
// Rendering
|
|
3108
3140
|
function renderCategoryFilter() {
|
|
3109
3141
|
const container = document.getElementById('categoryFilter');
|
|
3110
|
-
|
|
3142
|
+
const allCount = allAgents.length;
|
|
3143
|
+
container.innerHTML = `<div class="category-chip active" data-category="all" onclick="filterByCategory('all')"><i class="fas fa-th"></i> All (${allCount})</div>`;
|
|
3144
|
+
// Count agents per category
|
|
3145
|
+
const catCounts = {};
|
|
3146
|
+
allAgents.forEach(a => { catCounts[a.category] = (catCounts[a.category] || 0) + 1; });
|
|
3111
3147
|
Object.entries(categories).forEach(([key, cat]) => {
|
|
3148
|
+
const count = catCounts[key] || 0;
|
|
3112
3149
|
const chip = document.createElement('div');
|
|
3113
3150
|
chip.className = 'category-chip';
|
|
3114
3151
|
chip.dataset.category = key;
|
|
3115
|
-
chip.style.setProperty('--chip-color', cat.color);
|
|
3152
|
+
chip.style.setProperty('--chip-color', cat.color || '#8b949e');
|
|
3116
3153
|
chip.onclick = () => filterByCategory(key);
|
|
3117
|
-
chip.innerHTML = `<i class="fas fa-${getIconForCategory(cat.icon)}"></i> ${cat.name}`;
|
|
3154
|
+
chip.innerHTML = `<i class="fas fa-${getIconForCategory(cat.icon)}"></i> ${cat.name} (${count})`;
|
|
3118
3155
|
container.appendChild(chip);
|
|
3119
3156
|
});
|
|
3120
3157
|
}
|
|
3121
3158
|
|
|
3122
3159
|
function getIconForCategory(icon) {
|
|
3123
|
-
const iconMap = { 'code': 'code', 'shield-check': 'shield-alt', 'palette': 'palette', 'lightbulb': 'lightbulb', 'megaphone': 'bullhorn', 'settings': 'cog', 'chart-bar': 'chart-bar', 'cube': 'cube', 'search': 'search' };
|
|
3124
|
-
return iconMap[icon] || 'circle';
|
|
3160
|
+
const iconMap = { 'code': 'code', 'shield-check': 'shield-alt', 'palette': 'palette', 'lightbulb': 'lightbulb', 'megaphone': 'bullhorn', 'settings': 'cog', 'chart-bar': 'chart-bar', 'cube': 'cube', 'search': 'search', 'tasks': 'tasks', 'star': 'star', 'headset': 'headset', 'vial': 'vial', 'bullhorn': 'bullhorn', 'circle': 'circle' };
|
|
3161
|
+
return iconMap[icon] || icon || 'circle';
|
|
3125
3162
|
}
|
|
3126
3163
|
|
|
3127
3164
|
function renderAgents() {
|
|
@@ -3129,25 +3166,31 @@
|
|
|
3129
3166
|
const searchTerm = document.getElementById('agentSearch').value.toLowerCase();
|
|
3130
3167
|
let filteredAgents = allAgents;
|
|
3131
3168
|
if (activeCategory !== 'all') filteredAgents = filteredAgents.filter(a => a.category === activeCategory);
|
|
3132
|
-
if (searchTerm) filteredAgents = filteredAgents.filter(a => a.name.toLowerCase().includes(searchTerm) || a.description.toLowerCase().includes(searchTerm) || a.
|
|
3169
|
+
if (searchTerm) filteredAgents = filteredAgents.filter(a => a.name.toLowerCase().includes(searchTerm) || (a.description || '').toLowerCase().includes(searchTerm) || a.category.includes(searchTerm));
|
|
3133
3170
|
if (!filteredAgents.length) {
|
|
3134
|
-
grid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; padding: 60px; color: var(--text-secondary);">No agents found</div>';
|
|
3171
|
+
grid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; padding: 60px; color: var(--text-secondary);"><i class="fas fa-robot" style="font-size: 48px; margin-bottom: 16px; display: block; opacity: 0.3;"></i>No agents found</div>';
|
|
3135
3172
|
return;
|
|
3136
3173
|
}
|
|
3137
3174
|
grid.innerHTML = filteredAgents.map(agent => {
|
|
3138
|
-
const
|
|
3139
|
-
const
|
|
3140
|
-
const
|
|
3175
|
+
const isEnabled = agent.enabled;
|
|
3176
|
+
const catMeta = categories[agent.category] || {};
|
|
3177
|
+
const catColor = catMeta.color || agent.color || '#8b949e';
|
|
3178
|
+
const scopeClass = agent.scope === 'global' ? 'scope-global' : 'scope-project';
|
|
3179
|
+
const scopeLabel = agent.scope === 'global' ? 'Global' : 'Project';
|
|
3180
|
+
const catLabel = (catMeta.name || agent.category || '').replace(/-/g, ' ');
|
|
3141
3181
|
return `
|
|
3142
3182
|
<div class="agent-card ${isEnabled ? '' : 'disabled'}">
|
|
3143
|
-
<div class="agent-category-label" style="background: ${
|
|
3183
|
+
<div class="agent-category-label" style="background: ${catColor}"></div>
|
|
3144
3184
|
<div class="agent-card-header">
|
|
3145
|
-
<div class="agent-icon" style="background: ${
|
|
3185
|
+
<div class="agent-icon" style="background: ${catColor}20; color: ${catColor}"><i class="fas fa-${getIconForCategory(catMeta.icon)}"></i></div>
|
|
3146
3186
|
<div class="agent-toggle ${isEnabled ? 'active' : ''}" onclick="toggleAgent('${agent.id}', ${!isEnabled})"></div>
|
|
3147
3187
|
</div>
|
|
3148
3188
|
<div class="agent-name">${agent.name}</div>
|
|
3149
|
-
<div class="agent-description">${agent.description}</div>
|
|
3150
|
-
<div class="agent-
|
|
3189
|
+
<div class="agent-description">${agent.description || 'No description'}</div>
|
|
3190
|
+
<div class="agent-badges">
|
|
3191
|
+
<span class="agent-badge ${scopeClass}">${scopeLabel}</span>
|
|
3192
|
+
<span class="agent-badge category-badge">${catLabel}</span>
|
|
3193
|
+
</div>
|
|
3151
3194
|
</div>
|
|
3152
3195
|
`;
|
|
3153
3196
|
}).join('');
|
|
@@ -3197,18 +3240,18 @@
|
|
|
3197
3240
|
|
|
3198
3241
|
// Toggle actions
|
|
3199
3242
|
async function toggleAgent(agentId, enabled) {
|
|
3200
|
-
if (!currentProject) return;
|
|
3201
3243
|
try {
|
|
3202
|
-
const response = await fetch(`${API_URL}/api/
|
|
3244
|
+
const response = await fetch(`${API_URL}/api/agents/${agentId}/toggle`, {
|
|
3203
3245
|
method: 'POST',
|
|
3204
3246
|
headers: { 'Content-Type': 'application/json' },
|
|
3205
|
-
body: JSON.stringify({ enabled })
|
|
3247
|
+
body: JSON.stringify({ enabled, project_path: currentProject || undefined })
|
|
3206
3248
|
});
|
|
3207
3249
|
const result = await response.json();
|
|
3208
3250
|
if (result.success) {
|
|
3209
|
-
await
|
|
3210
|
-
renderAgents();
|
|
3251
|
+
await loadAgentData();
|
|
3211
3252
|
showToast(`${agentId} ${enabled ? 'enabled' : 'disabled'}`, 'success');
|
|
3253
|
+
} else {
|
|
3254
|
+
showToast(result.error || 'Failed to toggle agent', 'error');
|
|
3212
3255
|
}
|
|
3213
3256
|
} catch (e) {
|
|
3214
3257
|
showToast('Failed to update agent', 'error');
|
|
@@ -3254,21 +3297,28 @@
|
|
|
3254
3297
|
}
|
|
3255
3298
|
|
|
3256
3299
|
async function enableAllAgents(enabled) {
|
|
3257
|
-
|
|
3258
|
-
const
|
|
3259
|
-
|
|
3300
|
+
// Filter to only agents that need toggling
|
|
3301
|
+
const toToggle = allAgents.filter(a => a.enabled !== enabled);
|
|
3302
|
+
if (!toToggle.length) {
|
|
3303
|
+
showToast(`All agents already ${enabled ? 'enabled' : 'disabled'}`, 'info');
|
|
3304
|
+
return;
|
|
3305
|
+
}
|
|
3260
3306
|
try {
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3307
|
+
let succeeded = 0;
|
|
3308
|
+
// Toggle in batches of 5 for reasonable concurrency
|
|
3309
|
+
for (let i = 0; i < toToggle.length; i += 5) {
|
|
3310
|
+
const batch = toToggle.slice(i, i + 5);
|
|
3311
|
+
const results = await Promise.all(batch.map(agent =>
|
|
3312
|
+
fetch(`${API_URL}/api/agents/${agent.id}/toggle`, {
|
|
3313
|
+
method: 'POST',
|
|
3314
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3315
|
+
body: JSON.stringify({ enabled, project_path: currentProject || undefined })
|
|
3316
|
+
}).then(r => r.json())
|
|
3317
|
+
));
|
|
3318
|
+
succeeded += results.filter(r => r.success).length;
|
|
3271
3319
|
}
|
|
3320
|
+
await loadAgentData();
|
|
3321
|
+
showToast(`${succeeded} agents ${enabled ? 'enabled' : 'disabled'}`, 'success');
|
|
3272
3322
|
} catch (e) {
|
|
3273
3323
|
showToast('Failed to update agents', 'error');
|
|
3274
3324
|
}
|
|
@@ -261,12 +261,9 @@ def main():
|
|
|
261
261
|
if not response_text or len(response_text) < 50:
|
|
262
262
|
sys.exit(0)
|
|
263
263
|
|
|
264
|
-
# Load session data
|
|
265
|
-
session_data = load_session_data()
|
|
266
|
-
|
|
267
|
-
sys.exit(0)
|
|
268
|
-
|
|
269
|
-
session_id = session_data.get("session_id")
|
|
264
|
+
# Load session data, prefer session_id from stdin
|
|
265
|
+
session_data = load_session_data() or {}
|
|
266
|
+
session_id = hook_input.get("session_id") or session_data.get("session_id")
|
|
270
267
|
if not session_id:
|
|
271
268
|
sys.exit(0)
|
|
272
269
|
|
|
@@ -110,8 +110,8 @@ def main():
|
|
|
110
110
|
except (json.JSONDecodeError, ValueError, EOFError):
|
|
111
111
|
sys.exit(0)
|
|
112
112
|
|
|
113
|
-
# Get user message
|
|
114
|
-
user_message = hook_input.get("user_prompt", "")
|
|
113
|
+
# Get user message (Claude Code sends "prompt", legacy uses "user_prompt")
|
|
114
|
+
user_message = hook_input.get("prompt", "") or hook_input.get("user_prompt", "")
|
|
115
115
|
if not user_message:
|
|
116
116
|
session_messages = hook_input.get("session_messages", [])
|
|
117
117
|
if session_messages:
|
|
@@ -128,14 +128,15 @@ def main():
|
|
|
128
128
|
if not is_correction:
|
|
129
129
|
sys.exit(0)
|
|
130
130
|
|
|
131
|
-
# Load session data
|
|
132
|
-
session_data = load_session_data()
|
|
133
|
-
|
|
131
|
+
# Load session data, prefer session_id from stdin
|
|
132
|
+
session_data = load_session_data() or {}
|
|
133
|
+
stdin_session_id = hook_input.get("session_id")
|
|
134
|
+
if stdin_session_id and session_data.get("session_id") != stdin_session_id:
|
|
135
|
+
session_data["session_id"] = stdin_session_id
|
|
136
|
+
if not session_data.get("session_id"):
|
|
134
137
|
sys.exit(0)
|
|
135
138
|
|
|
136
139
|
session_id = session_data.get("session_id")
|
|
137
|
-
if not session_id:
|
|
138
|
-
sys.exit(0)
|
|
139
140
|
|
|
140
141
|
# Get the current request ID for causal chain linking
|
|
141
142
|
root_event_id = session_data.get("current_request_id")
|
package/hooks/grounding-hook.py
CHANGED
|
@@ -403,8 +403,24 @@ def check_and_trigger_flush(session_id: str, project_path: str):
|
|
|
403
403
|
|
|
404
404
|
def main():
|
|
405
405
|
"""Main entry point for the hook."""
|
|
406
|
-
|
|
407
|
-
|
|
406
|
+
# Read hook input from stdin (Claude Code sends session_id, cwd, prompt)
|
|
407
|
+
hook_input = {}
|
|
408
|
+
try:
|
|
409
|
+
hook_input = json.load(sys.stdin)
|
|
410
|
+
except Exception:
|
|
411
|
+
pass
|
|
412
|
+
|
|
413
|
+
project_path = hook_input.get("cwd") or get_project_path()
|
|
414
|
+
|
|
415
|
+
# Get session_id: stdin JSON > env var > .claude_session file
|
|
416
|
+
session_id = hook_input.get("session_id") or get_session_id()
|
|
417
|
+
|
|
418
|
+
# Ensure .claude_session file exists for sibling hooks
|
|
419
|
+
if session_id:
|
|
420
|
+
session_data = load_session_data() or {}
|
|
421
|
+
if session_data.get("session_id") != session_id:
|
|
422
|
+
session_data["session_id"] = session_id
|
|
423
|
+
save_session_data(session_data)
|
|
408
424
|
|
|
409
425
|
# If no session, try to init one
|
|
410
426
|
if not session_id:
|
|
@@ -475,7 +491,7 @@ def main():
|
|
|
475
491
|
curator_status = None
|
|
476
492
|
|
|
477
493
|
# Only fetch curator context if there's user input to contextualize
|
|
478
|
-
user_input = os.getenv("CLAUDE_USER_INPUT", "")
|
|
494
|
+
user_input = hook_input.get("prompt", "") or hook_input.get("user_prompt", "") or os.getenv("CLAUDE_USER_INPUT", "")
|
|
479
495
|
if user_input and len(user_input) > 10:
|
|
480
496
|
curator_summary = call_memory_agent("curator_get_summary", {
|
|
481
497
|
"query": user_input[:500], # Limit query length
|
package/hooks/log-tool-use.py
CHANGED
|
@@ -150,12 +150,9 @@ def main():
|
|
|
150
150
|
if tool_name not in TRACKABLE_TOOLS:
|
|
151
151
|
sys.exit(0)
|
|
152
152
|
|
|
153
|
-
# Load session data
|
|
154
|
-
session_data = load_session_data()
|
|
155
|
-
|
|
156
|
-
sys.exit(0)
|
|
157
|
-
|
|
158
|
-
session_id = session_data.get("session_id")
|
|
153
|
+
# Load session data, prefer session_id from stdin
|
|
154
|
+
session_data = load_session_data() or {}
|
|
155
|
+
session_id = hook_input.get("session_id") or session_data.get("session_id")
|
|
159
156
|
if not session_id:
|
|
160
157
|
sys.exit(0)
|
|
161
158
|
|
|
@@ -143,7 +143,8 @@ def main():
|
|
|
143
143
|
sys.exit(0)
|
|
144
144
|
|
|
145
145
|
# Get user message from hook input
|
|
146
|
-
|
|
146
|
+
# Claude Code sends "prompt", legacy format uses "user_prompt"
|
|
147
|
+
user_message = hook_input.get("prompt", "") or hook_input.get("user_prompt", "")
|
|
147
148
|
if not user_message:
|
|
148
149
|
# Try alternative format
|
|
149
150
|
session_messages = hook_input.get("session_messages", [])
|
|
@@ -155,15 +156,22 @@ def main():
|
|
|
155
156
|
if not user_message:
|
|
156
157
|
sys.exit(0)
|
|
157
158
|
|
|
158
|
-
#
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
# Get session_id: stdin JSON > env var > .claude_session file
|
|
160
|
+
session_id = hook_input.get("session_id") or os.getenv("CLAUDE_SESSION_ID")
|
|
161
|
+
session_data = load_session_data() or {}
|
|
162
|
+
|
|
163
|
+
if not session_id:
|
|
164
|
+
session_id = session_data.get("session_id")
|
|
162
165
|
|
|
163
|
-
session_id = session_data.get("session_id")
|
|
164
166
|
if not session_id:
|
|
165
167
|
sys.exit(0)
|
|
166
168
|
|
|
169
|
+
# Ensure .claude_session file exists for sibling hooks
|
|
170
|
+
if not session_data or session_data.get("session_id") != session_id:
|
|
171
|
+
session_data = session_data or {}
|
|
172
|
+
session_data["session_id"] = session_id
|
|
173
|
+
save_session_data(session_data)
|
|
174
|
+
|
|
167
175
|
# Truncate long messages
|
|
168
176
|
summary = user_message[:200]
|
|
169
177
|
if len(user_message) > 200:
|
|
@@ -161,12 +161,9 @@ def main():
|
|
|
161
161
|
if tool_name not in DECISION_WORTHY_TOOLS:
|
|
162
162
|
sys.exit(0)
|
|
163
163
|
|
|
164
|
-
# Load session data
|
|
165
|
-
session_data = load_session_data()
|
|
166
|
-
|
|
167
|
-
sys.exit(0)
|
|
168
|
-
|
|
169
|
-
session_id = session_data.get("session_id")
|
|
164
|
+
# Load session data, prefer session_id from stdin
|
|
165
|
+
session_data = load_session_data() or {}
|
|
166
|
+
session_id = hook_input.get("session_id") or session_data.get("session_id")
|
|
170
167
|
if not session_id:
|
|
171
168
|
sys.exit(0)
|
|
172
169
|
|
|
@@ -74,6 +74,15 @@ def main():
|
|
|
74
74
|
except ImportError:
|
|
75
75
|
pass
|
|
76
76
|
|
|
77
|
+
# ---------------------------------------------------------------
|
|
78
|
+
# Step 1.7: Trigger soul integration (merge fragments → soul_state)
|
|
79
|
+
# ---------------------------------------------------------------
|
|
80
|
+
if session_id and project_path:
|
|
81
|
+
try:
|
|
82
|
+
_trigger_soul_integration(session_id, project_path, timeout=5.0)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
print(f"[SessionEnd] Soul integration failed (non-fatal): {e}", file=sys.stderr)
|
|
85
|
+
|
|
77
86
|
# ---------------------------------------------------------------
|
|
78
87
|
# Step 1.5: Deregister from cross-session awareness
|
|
79
88
|
# ---------------------------------------------------------------
|
|
@@ -180,5 +189,33 @@ def _trigger_session_wrapup(session_id: str, project_path: str, timeout: float =
|
|
|
180
189
|
print(f"[SessionEnd] Flush API call failed: {e}", file=sys.stderr)
|
|
181
190
|
|
|
182
191
|
|
|
192
|
+
def _trigger_soul_integration(session_id: str, project_path: str, timeout: float = 5.0):
|
|
193
|
+
"""Trigger soul integration — merges session fragments into persistent soul_state."""
|
|
194
|
+
import urllib.request
|
|
195
|
+
import urllib.error
|
|
196
|
+
|
|
197
|
+
memory_agent_url = os.getenv("MEMORY_AGENT_URL", "http://localhost:8102")
|
|
198
|
+
|
|
199
|
+
payload = json.dumps({
|
|
200
|
+
"session_id": session_id,
|
|
201
|
+
"project_path": project_path,
|
|
202
|
+
}).encode("utf-8")
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
req = urllib.request.Request(
|
|
206
|
+
f"{memory_agent_url}/api/soul/integrate",
|
|
207
|
+
data=payload,
|
|
208
|
+
headers={"Content-Type": "application/json"},
|
|
209
|
+
method="POST"
|
|
210
|
+
)
|
|
211
|
+
with urllib.request.urlopen(req, timeout=min(timeout, 5.0)) as resp:
|
|
212
|
+
if resp.status == 200:
|
|
213
|
+
result = json.loads(resp.read().decode("utf-8"))
|
|
214
|
+
integrated = result.get("integrated", 0)
|
|
215
|
+
print(f"[SessionEnd] Soul integration complete: {integrated} fragments integrated.", file=sys.stderr)
|
|
216
|
+
except (urllib.error.URLError, urllib.error.HTTPError, OSError, TimeoutError) as e:
|
|
217
|
+
print(f"[SessionEnd] Soul integration API call failed: {e}", file=sys.stderr)
|
|
218
|
+
|
|
219
|
+
|
|
183
220
|
if __name__ == "__main__":
|
|
184
221
|
main()
|
package/hooks/session_start.py
CHANGED
|
@@ -86,6 +86,16 @@ async def load_session_context(project_path: str) -> str:
|
|
|
86
86
|
"""Load all relevant context for a session start."""
|
|
87
87
|
context_parts = []
|
|
88
88
|
|
|
89
|
+
# ============================================================
|
|
90
|
+
# SOUL LAYER: Load soul brief (personality + learning context)
|
|
91
|
+
# ============================================================
|
|
92
|
+
soul_brief = await call_rest_api("GET", "/api/soul/brief", params={
|
|
93
|
+
"project_path": project_path,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
if soul_brief and soul_brief.get("success") and soul_brief.get("brief"):
|
|
97
|
+
context_parts.append(soul_brief["brief"])
|
|
98
|
+
|
|
89
99
|
# ============================================================
|
|
90
100
|
# CROSS-SESSION AWARENESS: Register this session + catch-up
|
|
91
101
|
# ============================================================
|
package/hooks/stop_hook.py
CHANGED
|
@@ -305,6 +305,122 @@ def _store_memory(extraction: Dict[str, Any], project_path: Optional[str] = None
|
|
|
305
305
|
return False
|
|
306
306
|
|
|
307
307
|
|
|
308
|
+
# ---------------------------------------------------------------------------
|
|
309
|
+
# Soul fragment capture (lightweight regex → HTTP POST)
|
|
310
|
+
# ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
SOUL_PATTERNS = {
|
|
313
|
+
"decision_made": [
|
|
314
|
+
re.compile(
|
|
315
|
+
r"(?:let's|we'll|going to|chose to|decided to)\s+(?:use|go with|implement|try)\s+(.+)",
|
|
316
|
+
re.IGNORECASE,
|
|
317
|
+
),
|
|
318
|
+
re.compile(
|
|
319
|
+
r"(?:using|choosing|picked)\s+(\S+)\s+(?:because|since|for)",
|
|
320
|
+
re.IGNORECASE,
|
|
321
|
+
),
|
|
322
|
+
],
|
|
323
|
+
"preference_expressed": [
|
|
324
|
+
re.compile(
|
|
325
|
+
r"(?:I prefer|you should always|always use|never use|don't use|I like to)\s+(.+)",
|
|
326
|
+
re.IGNORECASE,
|
|
327
|
+
),
|
|
328
|
+
re.compile(
|
|
329
|
+
r"(?:remember to|make sure to|don't forget to)\s+(.+)",
|
|
330
|
+
re.IGNORECASE,
|
|
331
|
+
),
|
|
332
|
+
],
|
|
333
|
+
"error_resolved": [
|
|
334
|
+
re.compile(
|
|
335
|
+
r"(?:fixed|resolved|solved|the issue was|root cause)\s*:?\s*(.+)",
|
|
336
|
+
re.IGNORECASE,
|
|
337
|
+
),
|
|
338
|
+
re.compile(
|
|
339
|
+
r"(?:the (?:fix|solution) (?:was|is))\s+(.+)",
|
|
340
|
+
re.IGNORECASE,
|
|
341
|
+
),
|
|
342
|
+
],
|
|
343
|
+
"pattern_used": [
|
|
344
|
+
re.compile(
|
|
345
|
+
r"(?:same (?:approach|pattern|method) as)\s+(.+)",
|
|
346
|
+
re.IGNORECASE,
|
|
347
|
+
),
|
|
348
|
+
re.compile(
|
|
349
|
+
r"(?:like we did (?:for|in|with))\s+(.+)",
|
|
350
|
+
re.IGNORECASE,
|
|
351
|
+
),
|
|
352
|
+
],
|
|
353
|
+
"correction_received": [
|
|
354
|
+
re.compile(
|
|
355
|
+
r"(?:no,?\s+(?:actually|that's wrong|not like that)|(?:don't|stop)\s+(?:do|doing)\s+that)\s*[,:]?\s*(.+)",
|
|
356
|
+
re.IGNORECASE,
|
|
357
|
+
),
|
|
358
|
+
],
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _capture_soul_fragments(
|
|
363
|
+
text: str, session_id: str, project_path: str
|
|
364
|
+
):
|
|
365
|
+
"""Extract and POST soul fragments from response text.
|
|
366
|
+
|
|
367
|
+
Runs regex extraction, then fires HTTP POST for each fragment.
|
|
368
|
+
Budget: < 200ms total. Non-blocking — failures are silent.
|
|
369
|
+
"""
|
|
370
|
+
import urllib.request
|
|
371
|
+
import urllib.error
|
|
372
|
+
|
|
373
|
+
fragments = []
|
|
374
|
+
seen = set()
|
|
375
|
+
|
|
376
|
+
for fragment_type, patterns in SOUL_PATTERNS.items():
|
|
377
|
+
for pattern in patterns:
|
|
378
|
+
for match in pattern.finditer(text):
|
|
379
|
+
captured = match.group(1).strip() if match.lastindex else match.group(0).strip()
|
|
380
|
+
if len(captured) < 10 or len(captured) > 300:
|
|
381
|
+
continue
|
|
382
|
+
key = captured[:50].lower()
|
|
383
|
+
if key in seen:
|
|
384
|
+
continue
|
|
385
|
+
seen.add(key)
|
|
386
|
+
fragments.append({
|
|
387
|
+
"fragment_type": fragment_type,
|
|
388
|
+
"content": captured[:300],
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
if not fragments:
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
# POST each fragment (fire-and-forget, tight timeout)
|
|
395
|
+
captured_count = 0
|
|
396
|
+
for frag in fragments[:5]: # Cap at 5 fragments per response
|
|
397
|
+
try:
|
|
398
|
+
payload = json.dumps({
|
|
399
|
+
"session_id": session_id,
|
|
400
|
+
"project_path": project_path,
|
|
401
|
+
"fragment_type": frag["fragment_type"],
|
|
402
|
+
"content": frag["content"],
|
|
403
|
+
}).encode("utf-8")
|
|
404
|
+
|
|
405
|
+
req = urllib.request.Request(
|
|
406
|
+
f"{MEMORY_AGENT_URL}/api/soul/capture",
|
|
407
|
+
data=payload,
|
|
408
|
+
headers={"Content-Type": "application/json"},
|
|
409
|
+
method="POST",
|
|
410
|
+
)
|
|
411
|
+
with urllib.request.urlopen(req, timeout=0.5) as resp:
|
|
412
|
+
if resp.status == 200:
|
|
413
|
+
captured_count += 1
|
|
414
|
+
except (urllib.error.URLError, urllib.error.HTTPError, OSError, TimeoutError):
|
|
415
|
+
pass # Silent failure — don't block the hook
|
|
416
|
+
|
|
417
|
+
if captured_count > 0:
|
|
418
|
+
print(
|
|
419
|
+
f"[Stop] Soul fragments captured: {captured_count}/{len(fragments)}",
|
|
420
|
+
file=sys.stderr,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
|
|
308
424
|
# ---------------------------------------------------------------------------
|
|
309
425
|
# Main
|
|
310
426
|
# ---------------------------------------------------------------------------
|
|
@@ -353,6 +469,13 @@ def main():
|
|
|
353
469
|
if stored_hashes:
|
|
354
470
|
_save_cursor_hashes(session_id, stored_hashes)
|
|
355
471
|
|
|
472
|
+
# --- Soul fragment capture (lightweight, adds ~100ms) ---
|
|
473
|
+
elapsed = time.time() - start
|
|
474
|
+
if elapsed < TOTAL_TIME_BUDGET - 0.2:
|
|
475
|
+
_capture_soul_fragments(
|
|
476
|
+
response_text, session_id, project_path
|
|
477
|
+
)
|
|
478
|
+
|
|
356
479
|
elapsed_total = round(time.time() - start, 3)
|
|
357
480
|
print(
|
|
358
481
|
f"[Stop] session={session_id} "
|