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.
Files changed (34) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +12 -0
  3. package/bootstrap.cjs +19 -0
  4. package/claude-hooks/bash-call-enforcer.cjs +140 -0
  5. package/claude-hooks/settings.json +132 -0
  6. package/claude-hooks/specmem-drilldown-hook.cjs +49 -2
  7. package/claude-hooks/specmem-drilldown-hook.js +49 -2
  8. package/claude-hooks/specmem-drilldown-hook.js.bak +495 -0
  9. package/claude-hooks/specmem-precompact.cjs +13 -36
  10. package/claude-hooks/specmem-precompact.js +3 -7
  11. package/claude-hooks/specmem-search-enforcer.cjs +229 -0
  12. package/claude-hooks/specmem-search-tracker.cjs +71 -0
  13. package/claude-hooks/specmem-session-start.cjs +38 -50
  14. package/claude-hooks/specmem-session-start.js +19 -60
  15. package/dist/config.js +11 -16
  16. package/dist/db/connectionPoolGoBrrr.js +3 -3
  17. package/dist/index.js +21 -4
  18. package/dist/mcp/compactionProxy.js +21 -1
  19. package/dist/mcp/embeddingServerManager.js +15 -1
  20. package/dist/mcp/mcpProtocolHandler.js +22 -4
  21. package/dist/mcp/specMemServer.js +16 -3
  22. package/dist/mcp/toolRegistry.js +19 -21
  23. package/dist/tools/goofy/checkSyncStatus.js +14 -7
  24. package/dist/watcher/fileWatcher.js +57 -20
  25. package/dist/watcher/index.js +26 -0
  26. package/dist/watcher/syncChecker.js +11 -7
  27. package/package.json +1 -1
  28. package/scripts/global-postinstall.cjs +7 -2
  29. package/scripts/specmem-init.cjs +5 -0
  30. package/specmem/model-config.json +26 -6
  31. package/specmem/supervisord.conf +1 -1
  32. package/specmem/user-config.json +12 -0
  33. package/svg-sections/readme-install.svg +35 -29
  34. 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(-200).join('\n');
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(-200).join('\n');
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) - BRIEF version
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
- sections.push('```');
347
- // DON'T compress - just take first 500 chars of clean content
348
- sections.push(lastSession.content.slice(0, 500).trim());
349
- sections.push('```');
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
- // CONDENSED tool reminder - just names, no descriptions
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('**System:**');
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 output = formatOutput(memories, lastSession, projectPath, sessionId);
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
- // Prepend reminder for to read the compressed Chinese AND respond in English
490
- const prefixedOutput = `[SpecMem Context - Read this for continuity]
491
- IMPORTANT: You can read Traditional Chinese compression with 99%+ accuracy. ALWAYS respond in English.
492
- ${compressed}`;
493
-
494
- // Output - clean trailing whitespace
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(-300).join('\n'),
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(-300).join('\n');
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 > 200) {
777
- sessionContent = contentLines.slice(0, 50).join('\n') +
778
- `\n... [${contentLines.length - 100} lines omitted] ...\n` +
779
- contentLines.slice(-50).join('\n');
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
- if (process.env.SPECMEM_COMPRESS_SESSION === '1') {
789
- const compressedSession = compressHookOutput(sessionContent, {
790
- threshold: 0.60,
791
- minLength: 30,
792
- includeWarning: false // Warning already added at top level
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
- // SESSION START: Output full context WITHOUT compression
861
- // Compression makes debugging hard and context is injected at start when there's room
862
- // Only compress if SPECMEM_COMPRESS_SESSION is set
863
- if (process.env.SPECMEM_COMPRESS_SESSION === '1') {
864
- const compressedContext = compressHookOutput(context, {
865
- threshold: 0.70,
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: use unix socket dir as host for postgres connection
841
- // When SPECMEM_CONTAINER_MODE is set, or container run dir has postgres socket
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
- const containerSocketExists = (() => {
846
- try {
847
- if (fs.existsSync(path.join(containerRunDir, '.s.PGSQL.5432'))) return true;
848
- // Fallback: projectPath might BE the specmem dir
849
- const altRunDir = path.join(projectPath, 'run');
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: 20, // safe default - each project creates its own pool, 100 would exhaust PG max_connections
21
- minConnections: 5, // keep some warm connections fr
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: 30000, // 30 sec to establish connection
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 manages embedding server do NOT interfere.
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
- logger.debug('container mode active — brain manages embedding server, skipping self-heal restart');
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();