specmem-hardwicksoftware 3.7.30 → 3.7.32
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/CHANGELOG.md +22 -0
- package/README.md +12 -0
- package/bootstrap.cjs +19 -0
- package/claude-hooks/bash-call-enforcer.cjs +140 -0
- package/claude-hooks/settings.json +132 -0
- package/claude-hooks/specmem-drilldown-hook.cjs +49 -2
- package/claude-hooks/specmem-drilldown-hook.js +49 -2
- package/claude-hooks/specmem-drilldown-hook.js.bak +495 -0
- package/claude-hooks/specmem-precompact.cjs +13 -36
- package/claude-hooks/specmem-precompact.js +3 -7
- package/claude-hooks/specmem-search-enforcer.cjs +229 -0
- package/claude-hooks/specmem-search-tracker.cjs +71 -0
- package/claude-hooks/specmem-session-start.cjs +38 -50
- package/claude-hooks/specmem-session-start.js +19 -60
- package/dist/config.js +11 -16
- package/dist/db/connectionPoolGoBrrr.js +3 -3
- package/dist/index.js +21 -4
- package/dist/mcp/compactionProxy.js +21 -1
- package/dist/mcp/embeddingServerManager.js +15 -1
- package/dist/mcp/mcpProtocolHandler.js +22 -4
- package/dist/mcp/specMemServer.js +16 -3
- package/dist/mcp/toolRegistry.js +19 -21
- package/dist/tools/goofy/checkSyncStatus.js +14 -7
- package/dist/watcher/fileWatcher.js +57 -20
- package/dist/watcher/index.js +26 -0
- package/dist/watcher/syncChecker.js +11 -7
- package/package.json +1 -1
- package/scripts/global-postinstall.cjs +7 -2
- package/scripts/specmem-init.cjs +5 -0
- package/specmem/model-config.json +26 -6
- package/specmem/supervisord.conf +1 -1
- package/specmem/user-config.json +12 -0
- package/svg-sections/readme-install.svg +35 -29
- package/svg-sections/readme-whats-new.svg +120 -114
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SPECMEM SEARCH ENFORCER - PreToolUse Hook
|
|
4
|
+
* ==========================================
|
|
5
|
+
*
|
|
6
|
+
* HARD BLOCKS agents that skip SpecMem semantic search.
|
|
7
|
+
*
|
|
8
|
+
* Rules:
|
|
9
|
+
* 1. Agents CANNOT do ANYTHING until they've called find_memory or find_code_pointers at least once
|
|
10
|
+
* 2. Every 3 searches (Grep/Glob/Read), agents MUST call find_code_pointers again
|
|
11
|
+
* 3. After find_code_pointers, agents MUST drill_down before continuing
|
|
12
|
+
* 4. 2nd search in a cycle: WARNING injected
|
|
13
|
+
* 5. 3rd search in a cycle: HARD BLOCK (deny)
|
|
14
|
+
* 6. Tool calls and other non-search tools DO NOT reset the counter
|
|
15
|
+
* 7. Main session (non-agent) gets suggestions, not blocks
|
|
16
|
+
*
|
|
17
|
+
* State: /tmp/specmem-search-enforcer-{session}.json
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
// --- Agent detection (inline, no require chain issues) ---
|
|
24
|
+
function isAgent() {
|
|
25
|
+
const markers = [
|
|
26
|
+
process.env.CLAUDE_AGENT === 'true',
|
|
27
|
+
process.env.CLAUDE_AGENT_TYPE,
|
|
28
|
+
process.env.TASK_ID,
|
|
29
|
+
(process.env.CLAUDE_WORKTREE || '').length > 0,
|
|
30
|
+
(process.env.CLAUDE_SESSION_ID || '').includes('task-'),
|
|
31
|
+
];
|
|
32
|
+
return markers.some(Boolean);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// --- Config ---
|
|
36
|
+
const SEARCH_TOOLS = ['Grep', 'Glob', 'Read', 'Bash'];
|
|
37
|
+
const WRITE_TOOLS = ['Edit', 'Write'];
|
|
38
|
+
const ALL_BLOCKED_TOOLS = [...SEARCH_TOOLS, ...WRITE_TOOLS];
|
|
39
|
+
|
|
40
|
+
const SPECMEM_SEARCH_TOOLS = [
|
|
41
|
+
'mcp__specmem__find_memory',
|
|
42
|
+
'mcp__specmem__find_code_pointers',
|
|
43
|
+
'mcp__specmem__smart_search',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const SPECMEM_DRILLDOWN_TOOLS = [
|
|
47
|
+
'mcp__specmem__drill_down',
|
|
48
|
+
'mcp__specmem__get_memory',
|
|
49
|
+
'mcp__specmem__get_memory_by_id',
|
|
50
|
+
'mcp__specmem__getMemoryFull',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const SPECMEM_CODE_POINTER_TOOLS = [
|
|
54
|
+
'mcp__specmem__find_code_pointers',
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const SEARCH_CYCLE_LIMIT = 3; // block on 3rd search
|
|
58
|
+
const WARN_AT = 2; // warn on 2nd search
|
|
59
|
+
|
|
60
|
+
// --- State management ---
|
|
61
|
+
function getStateFile() {
|
|
62
|
+
const sessionId = process.env.CLAUDE_SESSION_ID || process.env.TASK_ID || 'default';
|
|
63
|
+
const sanitized = sessionId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
64
|
+
return `/tmp/specmem-search-enforcer-${sanitized}.json`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getState() {
|
|
68
|
+
try {
|
|
69
|
+
const f = getStateFile();
|
|
70
|
+
if (fs.existsSync(f)) {
|
|
71
|
+
const data = JSON.parse(fs.readFileSync(f, 'utf-8'));
|
|
72
|
+
// Expire after 30 min
|
|
73
|
+
if (data.timestamp && (Date.now() - data.timestamp > 30 * 60 * 1000)) {
|
|
74
|
+
return freshState();
|
|
75
|
+
}
|
|
76
|
+
return data;
|
|
77
|
+
}
|
|
78
|
+
} catch (e) {
|
|
79
|
+
try { fs.unlinkSync(getStateFile()); } catch (_) {}
|
|
80
|
+
}
|
|
81
|
+
return freshState();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function freshState() {
|
|
85
|
+
return {
|
|
86
|
+
hasUsedSpecmemSearch: false,
|
|
87
|
+
searchesSinceLastCodePointers: 0,
|
|
88
|
+
pendingDrilldown: false,
|
|
89
|
+
lastCodePointersQuery: null,
|
|
90
|
+
timestamp: Date.now(),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function saveState(state) {
|
|
95
|
+
try {
|
|
96
|
+
state.timestamp = Date.now();
|
|
97
|
+
fs.writeFileSync(getStateFile(), JSON.stringify(state, null, 2));
|
|
98
|
+
} catch (e) { /* silent */ }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// --- stdin reader with timeout ---
|
|
102
|
+
function readStdinWithTimeout(timeoutMs = 5000) {
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
let input = '';
|
|
105
|
+
const timer = setTimeout(() => {
|
|
106
|
+
process.stdin.destroy();
|
|
107
|
+
resolve(input);
|
|
108
|
+
}, timeoutMs);
|
|
109
|
+
process.stdin.setEncoding('utf8');
|
|
110
|
+
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
111
|
+
process.stdin.on('end', () => { clearTimeout(timer); resolve(input); });
|
|
112
|
+
process.stdin.on('error', () => { clearTimeout(timer); resolve(input); });
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// --- Main ---
|
|
117
|
+
async function main() {
|
|
118
|
+
const inputData = await readStdinWithTimeout(5000);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const hookData = JSON.parse(inputData);
|
|
122
|
+
const toolName = hookData.tool_name || '';
|
|
123
|
+
const toolInput = hookData.tool_input || {};
|
|
124
|
+
const state = getState();
|
|
125
|
+
|
|
126
|
+
// --- SpecMem search tool used (find_memory, find_code_pointers, smart_search) ---
|
|
127
|
+
if (SPECMEM_SEARCH_TOOLS.includes(toolName)) {
|
|
128
|
+
state.hasUsedSpecmemSearch = true;
|
|
129
|
+
|
|
130
|
+
// find_code_pointers resets the search counter AND sets drilldown pending
|
|
131
|
+
if (SPECMEM_CODE_POINTER_TOOLS.includes(toolName)) {
|
|
132
|
+
state.searchesSinceLastCodePointers = 0;
|
|
133
|
+
state.pendingDrilldown = true;
|
|
134
|
+
state.lastCodePointersQuery = toolInput.query || '(unknown)';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
saveState(state);
|
|
138
|
+
process.exit(0); // allow
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// --- Drilldown tool used (drill_down, get_memory, etc) ---
|
|
142
|
+
if (SPECMEM_DRILLDOWN_TOOLS.includes(toolName)) {
|
|
143
|
+
state.pendingDrilldown = false;
|
|
144
|
+
saveState(state);
|
|
145
|
+
process.exit(0); // allow
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Non-agent: suggest only, never block ---
|
|
149
|
+
if (!isAgent()) {
|
|
150
|
+
process.exit(0); // allow everything for main session
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// === AGENT ENFORCEMENT BELOW ===
|
|
154
|
+
|
|
155
|
+
// --- Rule 1: Agent hasn't used ANY specmem search yet -> HARD BLOCK everything ---
|
|
156
|
+
if (!state.hasUsedSpecmemSearch && ALL_BLOCKED_TOOLS.includes(toolName)) {
|
|
157
|
+
const output = {
|
|
158
|
+
hookSpecificOutput: {
|
|
159
|
+
hookEventName: 'PreToolUse',
|
|
160
|
+
permissionDecision: 'deny',
|
|
161
|
+
permissionDecisionReason: `BLOCKED: You MUST call find_memory or find_code_pointers BEFORE using ${toolName}. No Read/Write/Grep/Glob/Edit/Bash allowed until you search SpecMem first. Run: mcp__specmem__find_code_pointers({query: "your task description"})`
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
console.log(JSON.stringify(output));
|
|
165
|
+
process.exit(0);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// --- Rule 3: Pending drilldown after find_code_pointers -> BLOCK until drilled ---
|
|
169
|
+
if (state.pendingDrilldown && ALL_BLOCKED_TOOLS.includes(toolName)) {
|
|
170
|
+
const output = {
|
|
171
|
+
hookSpecificOutput: {
|
|
172
|
+
hookEventName: 'PreToolUse',
|
|
173
|
+
permissionDecision: 'deny',
|
|
174
|
+
permissionDecisionReason: `BLOCKED: You ran find_code_pointers("${state.lastCodePointersQuery}") but haven't drilled down into the results yet. You MUST call drill_down({drilldownID: N}) or get_memory({id: "ID"}) before using ${toolName}. Drill into the results first!`
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
console.log(JSON.stringify(output));
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// --- Count searches for cycle enforcement ---
|
|
182
|
+
if (SEARCH_TOOLS.includes(toolName)) {
|
|
183
|
+
state.searchesSinceLastCodePointers++;
|
|
184
|
+
saveState(state);
|
|
185
|
+
|
|
186
|
+
// Rule 5: 3rd search -> HARD BLOCK
|
|
187
|
+
if (state.searchesSinceLastCodePointers >= SEARCH_CYCLE_LIMIT) {
|
|
188
|
+
const output = {
|
|
189
|
+
hookSpecificOutput: {
|
|
190
|
+
hookEventName: 'PreToolUse',
|
|
191
|
+
permissionDecision: 'deny',
|
|
192
|
+
permissionDecisionReason: `BLOCKED: You've done ${state.searchesSinceLastCodePointers} searches without calling find_code_pointers. Every 3 searches you MUST call mcp__specmem__find_code_pointers to refresh your semantic context. Do it now before continuing.`
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
console.log(JSON.stringify(output));
|
|
196
|
+
process.exit(0);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Rule 4: 2nd search -> WARNING
|
|
200
|
+
if (state.searchesSinceLastCodePointers >= WARN_AT) {
|
|
201
|
+
const output = {
|
|
202
|
+
hookSpecificOutput: {
|
|
203
|
+
hookEventName: 'PreToolUse',
|
|
204
|
+
permissionDecision: 'allow',
|
|
205
|
+
permissionDecisionReason: `WARNING: ${state.searchesSinceLastCodePointers}/${SEARCH_CYCLE_LIMIT} searches used. You MUST call find_code_pointers before your next search or you'll be blocked. Consider running it now.`,
|
|
206
|
+
additionalContext: `\u26a0\ufe0f SEARCH LIMIT WARNING: ${state.searchesSinceLastCodePointers}/${SEARCH_CYCLE_LIMIT} searches since last find_code_pointers. Next search WILL BE BLOCKED. Run mcp__specmem__find_code_pointers now.`
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
console.log(JSON.stringify(output));
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// --- Write tools increment search counter too (they shouldn't write blind) ---
|
|
215
|
+
if (WRITE_TOOLS.includes(toolName)) {
|
|
216
|
+
// Don't count writes toward search limit, but they're allowed if we passed the checks above
|
|
217
|
+
saveState(state);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Allow everything else (Task, ToolSearch, MCP tools, etc)
|
|
221
|
+
process.exit(0);
|
|
222
|
+
|
|
223
|
+
} catch (error) {
|
|
224
|
+
// Parse failure = allow (don't break the session)
|
|
225
|
+
process.exit(0);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
main().catch(() => process.exit(0));
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SPECMEM SEARCH TRACKER - PostToolUse Hook
|
|
4
|
+
* ===========================================
|
|
5
|
+
*
|
|
6
|
+
* Tracks when agents use SpecMem semantic tools (find_memory, find_code_pointers, drill_down)
|
|
7
|
+
* and resets the search counter so enforcer unblocks.
|
|
8
|
+
*
|
|
9
|
+
* Also tracks Grep/Glob calls to increment search counter.
|
|
10
|
+
*
|
|
11
|
+
* AGENTS ONLY - main session skipped.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
// Agent detection
|
|
18
|
+
function isAgent() {
|
|
19
|
+
const e = process.env;
|
|
20
|
+
if (e.CLAUDE_AGENT === '1' || e.CLAUDE_AGENT === 'true') return true;
|
|
21
|
+
if (e.TASK_ID || e.AGENT_ID || e.WORKTREE_PATH) return true;
|
|
22
|
+
if (e.CLAUDE_CODE_ENTRYPOINT === 'task') return true;
|
|
23
|
+
const ppid = e.CLAUDE_PARENT_PID || e.PARENT_PID;
|
|
24
|
+
if (ppid && ppid !== '1' && ppid !== String(process.pid)) return true;
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function main() {
|
|
29
|
+
if (!isAgent()) {
|
|
30
|
+
console.log(JSON.stringify({ permissionDecision: 'allow' }));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const toolName = process.env.TOOL_NAME || '';
|
|
35
|
+
const sessionId = process.env.SESSION_ID || process.env.CLAUDE_SESSION_ID || process.ppid?.toString() || 'unknown';
|
|
36
|
+
const stateDir = '/tmp/specmem-enforcer';
|
|
37
|
+
const stateFile = path.join(stateDir, `${sessionId}.json`);
|
|
38
|
+
|
|
39
|
+
try { fs.mkdirSync(stateDir, { recursive: true }); } catch {}
|
|
40
|
+
|
|
41
|
+
let state = { searchCount: 0, specmemUsed: false, drilldownRequired: false, drilldownDone: false };
|
|
42
|
+
try { state = JSON.parse(fs.readFileSync(stateFile, 'utf8')); } catch {}
|
|
43
|
+
|
|
44
|
+
const isSpecmemSearch = /find_memory|find_code_pointers/i.test(toolName);
|
|
45
|
+
const isDrilldown = /drill_down/i.test(toolName);
|
|
46
|
+
const isSearchTool = /^(Grep|Glob)$/i.test(toolName);
|
|
47
|
+
|
|
48
|
+
if (isSpecmemSearch) {
|
|
49
|
+
state.specmemUsed = true;
|
|
50
|
+
state.searchCount = 0; // Reset search counter
|
|
51
|
+
// find_code_pointers requires drill_down after
|
|
52
|
+
if (/find_code_pointers/i.test(toolName)) {
|
|
53
|
+
state.drilldownRequired = true;
|
|
54
|
+
state.drilldownDone = false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (isDrilldown) {
|
|
59
|
+
state.drilldownDone = true;
|
|
60
|
+
state.drilldownRequired = false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (isSearchTool) {
|
|
64
|
+
state.searchCount = (state.searchCount || 0) + 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try { fs.writeFileSync(stateFile, JSON.stringify(state)); } catch {}
|
|
68
|
+
console.log(JSON.stringify({ permissionDecision: 'allow' }));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
main();
|
|
@@ -283,10 +283,10 @@ function getLastSessionOutput() {
|
|
|
283
283
|
|
|
284
284
|
let sessionContent;
|
|
285
285
|
if (startIdx === -1) {
|
|
286
|
-
sessionContent = lines.slice(-
|
|
286
|
+
sessionContent = lines.slice(-40).join('\n');
|
|
287
287
|
} else {
|
|
288
288
|
const outputLines = lines.slice(startIdx + 2, endIdx > 0 ? endIdx : undefined);
|
|
289
|
-
sessionContent = outputLines.slice(-
|
|
289
|
+
sessionContent = outputLines.slice(-40).join('\n');
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
// FILTER OUT GARBAGE - Skills, corrupted content, and non-conversation data
|
|
@@ -331,6 +331,14 @@ function getLastSessionOutput() {
|
|
|
331
331
|
* Only includes: last 10 interactions + condensed tool list
|
|
332
332
|
* NO skill injection, NO verbose schemas
|
|
333
333
|
*/
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Compact tool reminder
|
|
337
|
+
*/
|
|
338
|
+
function getToolReminder() {
|
|
339
|
+
return '## SpecMem Tools Available\n\n**Memory Search:** `find_memory` | `find_code_pointers` | `drill_down` | `get_memory`\n**Storage:** `save_memory` | `link_the_vibes` | `smush_memories_together`\n**Team:** `send_team_message` | `read_team_messages` | `claim_task` / `release_task`\n**System:** `show_me_the_stats` | `check_sync` | `start_watching`\n\nUse·`/specmem`·令④ⓢ.';
|
|
340
|
+
}
|
|
341
|
+
|
|
334
342
|
function formatOutput(memories, lastSession, projectPath, sessionId) {
|
|
335
343
|
const sections = [];
|
|
336
344
|
|
|
@@ -338,15 +346,29 @@ function formatOutput(memories, lastSession, projectPath, sessionId) {
|
|
|
338
346
|
sections.push(`Project: ${projectPath}`);
|
|
339
347
|
sections.push(`Session: ${sessionId}`);
|
|
340
348
|
|
|
341
|
-
// Previous session output (if available) -
|
|
349
|
+
// Previous session output (if available) - compressed
|
|
342
350
|
if (lastSession && lastSession.content) {
|
|
343
351
|
sections.push('');
|
|
344
352
|
sections.push(`## PREVIOUS SESSION OUTPUT (${lastSession.ageMinutes}m ago, reason: ${lastSession.reason})`);
|
|
345
353
|
sections.push('(Previous session):');
|
|
346
|
-
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
|
|
354
|
+
|
|
355
|
+
// Truncate to 40 lines for token efficiency
|
|
356
|
+
const contentLines = lastSession.content.split('\n');
|
|
357
|
+
let sessionContent;
|
|
358
|
+
if (contentLines.length > 40) {
|
|
359
|
+
sessionContent = contentLines.slice(0, 15).join('\n') +
|
|
360
|
+
`\n... [${contentLines.length - 25} lines omitted] ...\n` +
|
|
361
|
+
contentLines.slice(-10).join('\n');
|
|
362
|
+
} else {
|
|
363
|
+
sessionContent = lastSession.content;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const compressedSession = compressHookOutput(sessionContent, {
|
|
367
|
+
threshold: 0.60,
|
|
368
|
+
minLength: 30,
|
|
369
|
+
includeWarning: false
|
|
370
|
+
});
|
|
371
|
+
sections.push(compressedSession.trim());
|
|
350
372
|
}
|
|
351
373
|
|
|
352
374
|
// Recent memories - last 10 user/claude interactions ONLY
|
|
@@ -360,34 +382,9 @@ function formatOutput(memories, lastSession, projectPath, sessionId) {
|
|
|
360
382
|
});
|
|
361
383
|
}
|
|
362
384
|
|
|
363
|
-
//
|
|
364
|
-
sections.push('');
|
|
365
|
-
sections.push('## SpecMem Tools Available');
|
|
366
|
-
sections.push('');
|
|
367
|
-
sections.push('**Memory Search:**');
|
|
368
|
-
sections.push('- `find_memory` - Semantic search across all memories');
|
|
369
|
-
sections.push('- `find_code_pointers` - Search code with tracebacks');
|
|
370
|
-
sections.push('- `drill_down` - Explore memory details');
|
|
371
|
-
sections.push('- `get_memory` - Get specific memory by ID');
|
|
372
|
-
sections.push('');
|
|
373
|
-
sections.push('**Memory Storage:**');
|
|
374
|
-
sections.push('- `save_memory` - Store new memories');
|
|
375
|
-
sections.push('- `link_the_vibes` - Connect related memories');
|
|
376
|
-
sections.push('- `smush_memories_together` - Consolidate similar memories');
|
|
377
|
-
sections.push('');
|
|
378
|
-
sections.push('**Team Comms:**');
|
|
379
|
-
sections.push('- `send_team_message` - Message team members');
|
|
380
|
-
sections.push('- `read_team_messages` - Check team updates');
|
|
381
|
-
sections.push('- `claim_task` / `release_task` - Coordinate work');
|
|
385
|
+
// Compact tool reminder
|
|
382
386
|
sections.push('');
|
|
383
|
-
sections.push(
|
|
384
|
-
sections.push('- `show_me_the_stats` - Memory statistics');
|
|
385
|
-
sections.push('- `check_sync` - Verify file sync status');
|
|
386
|
-
sections.push('- `start_watching` - Enable file watcher');
|
|
387
|
-
sections.push('');
|
|
388
|
-
sections.push('Use `/specmem` for all commands.');
|
|
389
|
-
sections.push('');
|
|
390
|
-
sections.push('**Chinese compression:** You can read Traditional Chinese compression with 99%+ accuracy.');
|
|
387
|
+
sections.push(getToolReminder());
|
|
391
388
|
sections.push('[/SPECMEM-SESSION]');
|
|
392
389
|
|
|
393
390
|
return sections.join('\n');
|
|
@@ -476,23 +473,14 @@ async function main() {
|
|
|
476
473
|
const lastSession = getLastSessionOutput();
|
|
477
474
|
|
|
478
475
|
// Format output
|
|
479
|
-
let
|
|
480
|
-
|
|
481
|
-
// Apply Traditional Chinese compression to final output
|
|
482
|
-
const compressed = compressHookOutput(output, {
|
|
483
|
-
threshold: 0.50,
|
|
484
|
-
minLength: 100,
|
|
485
|
-
preserveStructure: true,
|
|
486
|
-
includeWarning: false // We add our own prefix below
|
|
487
|
-
});
|
|
476
|
+
let context = formatOutput(memories, lastSession, projectPath, sessionId);
|
|
488
477
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
console.log(prefixedOutput.split('\n').map(l => l.trimEnd()).join('\n').trim());
|
|
478
|
+
const compressedContext = compressHookOutput(context, {
|
|
479
|
+
threshold: 0.70,
|
|
480
|
+
minLength: 50,
|
|
481
|
+
includeWarning: true
|
|
482
|
+
});
|
|
483
|
+
console.log(compressedContext.trim());
|
|
496
484
|
|
|
497
485
|
process.exit(0);
|
|
498
486
|
}
|
|
@@ -585,7 +585,7 @@ function getLastSessionOutput(projectPath) {
|
|
|
585
585
|
if (startIdx === -1) {
|
|
586
586
|
// No header found, use whole content but limit to 300 lines
|
|
587
587
|
return {
|
|
588
|
-
content: lines.slice(-
|
|
588
|
+
content: lines.slice(-40).join('\n'),
|
|
589
589
|
ageMinutes,
|
|
590
590
|
reason: lines.find(l => l.startsWith('# Reason:'))?.replace('# Reason:', '').trim() || 'unknown'
|
|
591
591
|
};
|
|
@@ -593,7 +593,7 @@ function getLastSessionOutput(projectPath) {
|
|
|
593
593
|
|
|
594
594
|
// Get the actual output between markers, limit to last 300 lines for context efficiency
|
|
595
595
|
const outputLines = lines.slice(startIdx + 2, endIdx > 0 ? endIdx : undefined);
|
|
596
|
-
const trimmedOutput = outputLines.slice(-
|
|
596
|
+
const trimmedOutput = outputLines.slice(-40).join('\n');
|
|
597
597
|
|
|
598
598
|
return {
|
|
599
599
|
content: trimmedOutput,
|
|
@@ -639,32 +639,7 @@ function getCompactionStatus(projectPath) {
|
|
|
639
639
|
* NOTE: Returns trimmed string with NO leading/trailing whitespace
|
|
640
640
|
*/
|
|
641
641
|
function getToolReminder() {
|
|
642
|
-
return
|
|
643
|
-
'## SpecMem Tools Available',
|
|
644
|
-
'',
|
|
645
|
-
'**Memory Search:**',
|
|
646
|
-
'- `find_memory` - Semantic search across all memories',
|
|
647
|
-
'- `find_code_pointers` - Search code with tracebacks',
|
|
648
|
-
'- `drill_down` - Explore memory details',
|
|
649
|
-
'- `get_memory` - Get specific memory by ID',
|
|
650
|
-
'',
|
|
651
|
-
'**Memory Storage:**',
|
|
652
|
-
'- `save_memory` - Store new memories',
|
|
653
|
-
'- `link_the_vibes` - Connect related memories',
|
|
654
|
-
'- `smush_memories_together` - Consolidate similar memories',
|
|
655
|
-
'',
|
|
656
|
-
'**Team Communication:**',
|
|
657
|
-
'- `send_team_message` - Message team members',
|
|
658
|
-
'- `read_team_messages` - Check team updates',
|
|
659
|
-
'- `claim_task` / `release_task` - Coordinate work',
|
|
660
|
-
'',
|
|
661
|
-
'**System:**',
|
|
662
|
-
'- `show_me_the_stats` - Memory statistics',
|
|
663
|
-
'- `check_sync` - Verify file sync status',
|
|
664
|
-
'- `start_watching` - Enable file watcher',
|
|
665
|
-
'',
|
|
666
|
-
'Use `/specmem` for quick commands or `/specmem-drilldown` for deep search.'
|
|
667
|
-
].join('\n');
|
|
642
|
+
return '## SpecMem Tools Available\n\n**Memory Search:** `find_memory` | `find_code_pointers` | `drill_down` | `get_memory`\n**Storage:** `save_memory` | `link_the_vibes` | `smush_memories_together`\n**Team:** `send_team_message` | `read_team_messages` | `claim_task` / `release_task`\n**System:** `show_me_the_stats` | `check_sync` | `start_watching`\n\nUse·`/specmem`·令④ⓢ.';
|
|
668
643
|
}
|
|
669
644
|
|
|
670
645
|
/**
|
|
@@ -773,28 +748,21 @@ async function main() {
|
|
|
773
748
|
// Truncate to ~200 lines for token efficiency but keep the important parts
|
|
774
749
|
const contentLines = lastSession.content.split('\n');
|
|
775
750
|
let sessionContent;
|
|
776
|
-
if (contentLines.length >
|
|
777
|
-
sessionContent = contentLines.slice(0,
|
|
778
|
-
`\n... [${contentLines.length -
|
|
779
|
-
contentLines.slice(-
|
|
751
|
+
if (contentLines.length > 40) {
|
|
752
|
+
sessionContent = contentLines.slice(0, 15).join('\n') +
|
|
753
|
+
`\n... [${contentLines.length - 25} lines omitted] ...\n` +
|
|
754
|
+
contentLines.slice(-10).join('\n');
|
|
780
755
|
} else {
|
|
781
756
|
sessionContent = lastSession.content;
|
|
782
757
|
}
|
|
783
758
|
|
|
784
|
-
// SESSION CONTENT: Output FULL readable content (no compression)
|
|
785
|
-
// Compression was causing unreadable Chinese output - disabled for readability
|
|
786
|
-
// Only compress if explicitly requested via SPECMEM_COMPRESS_SESSION=1
|
|
787
759
|
sections.push('```');
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
sections.push(compressedSession.trim());
|
|
795
|
-
} else {
|
|
796
|
-
sections.push(sessionContent.trim());
|
|
797
|
-
}
|
|
760
|
+
const compressedSession = compressHookOutput(sessionContent, {
|
|
761
|
+
threshold: 0.60,
|
|
762
|
+
minLength: 30,
|
|
763
|
+
includeWarning: false
|
|
764
|
+
});
|
|
765
|
+
sections.push(compressedSession.trim());
|
|
798
766
|
sections.push('```');
|
|
799
767
|
}
|
|
800
768
|
|
|
@@ -857,21 +825,12 @@ async function main() {
|
|
|
857
825
|
.replace(/\n{3,}/g, '\n\n') // Collapse 3+ newlines to 2 (one blank line)
|
|
858
826
|
.trim(); // Remove leading/trailing whitespace
|
|
859
827
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
minLength: 50,
|
|
867
|
-
includeWarning: true
|
|
868
|
-
});
|
|
869
|
-
// Final trim to ensure no trailing whitespace
|
|
870
|
-
console.log(compressedContext.trim());
|
|
871
|
-
} else {
|
|
872
|
-
// Output full readable context (already trimmed above)
|
|
873
|
-
console.log(context);
|
|
874
|
-
}
|
|
828
|
+
const compressedContext = compressHookOutput(context, {
|
|
829
|
+
threshold: 0.70,
|
|
830
|
+
minLength: 50,
|
|
831
|
+
includeWarning: true
|
|
832
|
+
});
|
|
833
|
+
console.log(compressedContext.trim());
|
|
875
834
|
|
|
876
835
|
process.exit(0);
|
|
877
836
|
}
|
package/dist/config.js
CHANGED
|
@@ -837,24 +837,19 @@ export function loadConfig() {
|
|
|
837
837
|
const parsedUrl = parseDatabaseUrl();
|
|
838
838
|
// Priority: DATABASE_URL > ENV VAR > .specmemrc > default
|
|
839
839
|
// Per-project isolation still applies if DATABASE_URL not set
|
|
840
|
-
// Container mode:
|
|
841
|
-
//
|
|
842
|
-
// FIX: Try both {projectPath}/specmem/run and {projectPath}/run to handle
|
|
843
|
-
// the case where projectPath IS the specmem dir (avoids specmem/specmem/run)
|
|
840
|
+
// Container mode: postgres via unix socket in specmem/run/ (bind-mounted from container /data/run)
|
|
841
|
+
// Socket appears after container starts PG — dir must exist, socket arrives when PG is ready
|
|
844
842
|
let containerRunDir = path.join(projectPath, 'specmem', 'run');
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
if (fs.existsSync(path.join(altRunDir, '.s.PGSQL.5432'))) {
|
|
851
|
-
containerRunDir = altRunDir;
|
|
852
|
-
return true;
|
|
853
|
-
}
|
|
854
|
-
return false;
|
|
855
|
-
} catch { return false; }
|
|
856
|
-
})();
|
|
843
|
+
// Also check projectPath/run in case projectPath IS the specmem dir
|
|
844
|
+
if (!fs.existsSync(containerRunDir) && fs.existsSync(path.join(projectPath, 'run'))) {
|
|
845
|
+
containerRunDir = path.join(projectPath, 'run');
|
|
846
|
+
}
|
|
847
|
+
const containerSocketExists = fs.existsSync(path.join(containerRunDir, '.s.PGSQL.5432'));
|
|
857
848
|
const isContainerMode = process.env['SPECMEM_CONTAINER_MODE'] === 'true' || containerSocketExists;
|
|
849
|
+
if (isContainerMode) {
|
|
850
|
+
// Ensure socket directory exists on host — container bind-mounts here
|
|
851
|
+
try { fs.mkdirSync(containerRunDir, { recursive: true }); } catch (e) { /* may already exist */ }
|
|
852
|
+
}
|
|
858
853
|
const defaultDbHost = isContainerMode ? containerRunDir : 'localhost';
|
|
859
854
|
const dbHost = parsedUrl?.host || process.env['SPECMEM_DB_HOST'] || getRcValue(rc, 'database.host', defaultDbHost);
|
|
860
855
|
const dbPort = parsedUrl?.port || projectDbPort;
|
|
@@ -17,10 +17,10 @@ types.setTypeParser(20, (val) => {
|
|
|
17
17
|
return Number.isSafeInteger(n) ? n : BigInt(val);
|
|
18
18
|
}); // bigint - safe for values > 2^53
|
|
19
19
|
const DEFAULT_POOL_SETTINGS = {
|
|
20
|
-
maxConnections:
|
|
21
|
-
minConnections:
|
|
20
|
+
maxConnections: 6, // tuned for 4-core 8GB laptop - 20 was exhausting PG under concurrent tool calls
|
|
21
|
+
minConnections: 2, // keep a couple warm, don't hog connections
|
|
22
22
|
idleTimeoutMs: 30000, // 30 sec timeout on idle connections
|
|
23
|
-
connectionTimeoutMs:
|
|
23
|
+
connectionTimeoutMs: 10000, // 10 sec to establish connection - fail fast under load
|
|
24
24
|
statementTimeoutMs: 30000, // 30 sec statement timeout
|
|
25
25
|
queryTimeoutMs: 60000, // 1 min query timeout for thicc queries
|
|
26
26
|
healthCheckIntervalMs: 30000, // health check every 30 sec
|
package/dist/index.js
CHANGED
|
@@ -1608,11 +1608,28 @@ class LocalEmbeddingProvider {
|
|
|
1608
1608
|
* Runs in background to not block embedding requests
|
|
1609
1609
|
*/
|
|
1610
1610
|
tryRestartContainer() {
|
|
1611
|
-
// Container mode: brain container
|
|
1612
|
-
// Self-healing attempts override sandboxSocketPath to wrong path (embeddings.sock vs embed.sock)
|
|
1613
|
-
// and try to start conflicting Docker containers, causing CPU/RAM waste and socket confusion.
|
|
1611
|
+
// Container mode: restart the brain container instead of spawning a new process
|
|
1614
1612
|
if (process.env.SPECMEM_CONTAINER_MODE === 'true') {
|
|
1615
|
-
|
|
1613
|
+
const now = Date.now();
|
|
1614
|
+
if (now - this.lastRestartAttempt < LocalEmbeddingProvider.RESTART_COOLDOWN_MS) {
|
|
1615
|
+
logger.debug('container restart cooldown active, skipping');
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
this.lastRestartAttempt = now;
|
|
1619
|
+
try {
|
|
1620
|
+
const { getContainerManager } = require('./container/containerManager.js');
|
|
1621
|
+
const projectPath = process.env.SPECMEM_PROJECT_PATH || process.cwd();
|
|
1622
|
+
const cm = getContainerManager(projectPath);
|
|
1623
|
+
logger.info({ projectPath }, '[LocalEmbeddingProvider] Restarting brain container...');
|
|
1624
|
+
cm.start().then(() => {
|
|
1625
|
+
logger.info('[LocalEmbeddingProvider] Brain container restarted');
|
|
1626
|
+
this.restartAttempts = 0;
|
|
1627
|
+
}).catch(err => {
|
|
1628
|
+
logger.error({ error: err?.message }, '[LocalEmbeddingProvider] Brain container restart failed');
|
|
1629
|
+
});
|
|
1630
|
+
} catch (err) {
|
|
1631
|
+
logger.error({ error: err?.message }, '[LocalEmbeddingProvider] Failed to get container manager');
|
|
1632
|
+
}
|
|
1616
1633
|
return;
|
|
1617
1634
|
}
|
|
1618
1635
|
const now = Date.now();
|