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
@@ -58,7 +58,7 @@ const TOOL_USE_INPUT_PREVIEW_CHARS = 100;
58
58
 
59
59
  // Old-message stripping config — applied to ALL requests (not just compaction)
60
60
  // Only strips tool_results larger than this threshold in messages outside the recent window
61
- const OLD_STRIP_THRESHOLD = 400; // chars — only strip results bigger than this
61
+ const OLD_STRIP_THRESHOLD = 100; // chars — only strip results bigger than this
62
62
  const OLD_STRIP_PREVIEW_CHARS = 200; // chars — keep this much of the original
63
63
 
64
64
  // Live neural MT compression config
@@ -533,6 +533,23 @@ function stripOldToolResults(messages) {
533
533
  return stripped;
534
534
  }
535
535
 
536
+ // Strip specmem hook injection text blocks from old messages
537
+ if (block.type === 'text' && typeof block.text === 'string') {
538
+ const txt = block.text;
539
+ const isHookOutput = txt.includes('[SM-找]') || txt.includes('[SM-碼]') ||
540
+ txt.includes('[SM-檔改]') || txt.includes('[SPECMEM-SESSION]') ||
541
+ txt.includes('⚠️壓縮:繁中') || txt.includes('[SM-搜]');
542
+ if (isHookOutput && txt.length > liveConfig.OLD_STRIP_THRESHOLD) {
543
+ const removed = txt.length - OLD_STRIP_PREVIEW_CHARS;
544
+ charsRemoved += removed;
545
+ toolResultsStripped++;
546
+ return {
547
+ ...block,
548
+ text: txt.slice(0, OLD_STRIP_PREVIEW_CHARS) + `...\n[HOOK-TRIMMED: ${txt.length} chars]`
549
+ };
550
+ }
551
+ }
552
+
536
553
  return block;
537
554
  });
538
555
 
@@ -789,6 +806,9 @@ async function compressMessagesLive(messages) {
789
806
 
790
807
  // Standalone text blocks (user/assistant conversational text) → steno only
791
808
  if (block.type === 'text' && typeof block.text === 'string') {
809
+ // Skip specmem hook injection text — already partially compressed Chinese, steno would garble
810
+ const _htxt = block.text;
811
+ if (_htxt.includes('[SM-') || _htxt.includes('[SPECMEM-SESSION]') || _htxt.includes('⚠️壓縮:繁中')) continue;
792
812
  if (looksLikeNaturalLanguage(block.text)) {
793
813
  originals.push(block.text);
794
814
  textLocations.push({ msgIdx: i, blockIdx: j, mtEligible: false });
@@ -2313,8 +2313,22 @@ export class EmbeddingServerManager extends EventEmitter {
2313
2313
  logger.warn({
2314
2314
  failures: this.config.maxFailuresBeforeRestart,
2315
2315
  restartCount: this.restartCount,
2316
- }, '[EmbeddingServerManager] Too many consecutive failures in container mode - cannot restart container process from host');
2316
+ }, '[EmbeddingServerManager] Too many consecutive failures in container mode - attempting container restart');
2317
2317
  this.emit('unhealthy_container', { failures: this.config.maxFailuresBeforeRestart });
2318
+ // FIX: Auto-restart brain container when embedding is dead
2319
+ try {
2320
+ const { getContainerManager } = require('../container/containerManager.js');
2321
+ const projectPath = process.env['SPECMEM_PROJECT_PATH'] || process.cwd();
2322
+ const cm = getContainerManager(projectPath);
2323
+ logger.info({ projectPath }, '[EmbeddingServerManager] Restarting brain container...');
2324
+ await cm.start();
2325
+ logger.info('[EmbeddingServerManager] Brain container restarted successfully');
2326
+ this.isRunning = true;
2327
+ this.startTime = Date.now();
2328
+ } catch (containerErr) {
2329
+ logger.error({ error: containerErr?.message || containerErr },
2330
+ '[EmbeddingServerManager] Failed to restart brain container');
2331
+ }
2318
2332
  }
2319
2333
  else {
2320
2334
  logger.warn({
@@ -424,12 +424,30 @@ export class MCPProtocolHandler {
424
424
  }
425
425
  }
426
426
  /**
427
- * batch handle multiple tool calls - for efficiency
427
+ * batch handle multiple tool calls - with concurrency limit
428
+ * prevents overwhelming the db pool when claude fires 5+ calls at once
428
429
  */
429
430
  async handleBatchToolCalls(calls) {
430
431
  const results = [];
431
- // process in parallel for speed
432
- const promises = calls.map(async (call) => {
432
+ // inline concurrency limiter - no npm deps needed
433
+ // max 2 concurrent to leave headroom on 4-core/8GB systems
434
+ const _limitConcurrency = (concurrency) => {
435
+ let active = 0;
436
+ const queue = [];
437
+ const next = () => {
438
+ while (active < concurrency && queue.length > 0) {
439
+ active++;
440
+ const { fn, resolve, reject } = queue.shift();
441
+ fn().then(resolve, reject).finally(() => { active--; next(); });
442
+ }
443
+ };
444
+ return (fn) => new Promise((resolve, reject) => {
445
+ queue.push({ fn, resolve, reject });
446
+ next();
447
+ });
448
+ };
449
+ const limit = _limitConcurrency(2);
450
+ const promises = calls.map((call) => limit(async () => {
433
451
  try {
434
452
  const result = await this.handleToolCall(call.name, call.args);
435
453
  return { name: call.name, result };
@@ -440,7 +458,7 @@ export class MCPProtocolHandler {
440
458
  error: error instanceof Error ? error.message : 'unknown error'
441
459
  };
442
460
  }
443
- });
461
+ }));
444
462
  const settled = await Promise.allSettled(promises);
445
463
  for (const result of settled) {
446
464
  if (result.status === 'fulfilled') {
@@ -236,8 +236,9 @@ export class SpecMemServer {
236
236
  this.announceToOnStartup();
237
237
  // Auto-start Codebook Learner (resource-capped background service)
238
238
  this._startCodebookLearner();
239
- // Auto-trigger background codebase indexing (populates code_definitions)
240
- // Runs deferred (10s delay) so it doesn't block MCP startup
239
+ // NOTE: _triggerCodebaseIndexing() is also called from deferredInitPromise.then()
240
+ // after DB migrations complete (which create codebase_files table).
241
+ // This early call may no-op if DB not ready yet — the post-DB call is the critical one.
241
242
  this._triggerCodebaseIndexing();
242
243
  };
243
244
  // get that db connection no cap
@@ -1285,6 +1286,14 @@ export class SpecMemServer {
1285
1286
  await this.initializeMiniCOTServerManager();
1286
1287
  startupLog('Mini COT server manager initialized');
1287
1288
  logger.info('SpecMem MCP server fully initialized — all components ready');
1289
+ // FIX: Trigger codebase indexing AFTER DB init (migrations create codebase_files table)
1290
+ // Previously only called in oninitialized which fires BEFORE deferred DB init,
1291
+ // causing checkCodebaseIndexStatus to see missing table → needsReindex=false → skip
1292
+ try {
1293
+ await this._triggerCodebaseIndexing();
1294
+ } catch (indexErr) {
1295
+ logger.warn({ error: indexErr?.message }, 'Post-DB-init codebase indexing failed (non-fatal)');
1296
+ }
1288
1297
  // Run initial sync on startup — ensures codebase is fresh when Claude Code launches
1289
1298
  await this._runStartupSync();
1290
1299
  // Start idle sync timer — auto-syncs when no tool calls for 60s
@@ -1354,7 +1363,11 @@ export class SpecMemServer {
1354
1363
  const checkResult = await CheckSyncStatus.execute({ detailed: false }, wm);
1355
1364
  const syncScore = checkResult?.syncScore ?? 100;
1356
1365
  // Only resync if drift detected (score < 100)
1357
- if (syncScore < 100) {
1366
+ // Skip resync if indexing is still pending (syncScore === -1)
1367
+ if (checkResult?.indexingPending) {
1368
+ process.stderr.write(`[SPECMEM IDLE-SYNC] Indexing still in progress, skipping resync\n`);
1369
+ }
1370
+ else if (syncScore < 100) {
1358
1371
  process.stderr.write(`[SPECMEM IDLE-SYNC] Drift detected (${syncScore}%), resyncing...\n`);
1359
1372
  const resyncResult = await ForceResync.execute({ dryRun: false }, wm);
1360
1373
  const added = resyncResult?.stats?.filesAdded ?? 0;
@@ -94,32 +94,30 @@ _cacheCleanupTimer.unref();
94
94
  /**
95
95
  * Get the project-scoped embedding cache
96
96
  */
97
- // HIGH-4: Simple lock to prevent concurrent eviction
98
- let _evictionInProgress = false;
97
+ // HIGH-4: Eviction uses while-loop to guarantee room before creating new entry.
98
+ // Old boolean _evictionInProgress flag was broken: when flag was true, eviction was
99
+ // skipped but new entry was still created at line 125, exceeding the 20-project limit.
99
100
  function getProjectEmbeddingCache() {
100
101
  const project = getProjectPath();
101
102
  _EMBEDDING_CACHE_ACCESS_TIMES.set(project, Date.now());
102
103
  if (!_EMBEDDING_CACHE_BY_PROJECT.has(project)) {
103
- // HIGH-4: Changed > to >= to evict at limit, not after. Added lock to prevent concurrent eviction.
104
- if (_EMBEDDING_CACHE_BY_PROJECT.size >= 20 && !_evictionInProgress) {
105
- _evictionInProgress = true;
106
- try {
107
- // Evict the least recently accessed project cache
108
- let oldestProject = null;
109
- let oldestTime = Infinity;
110
- for (const [p, t] of _EMBEDDING_CACHE_ACCESS_TIMES) {
111
- if (t < oldestTime) {
112
- oldestTime = t;
113
- oldestProject = p;
114
- }
104
+ // Evict until there's room - loop guarantees we never exceed limit
105
+ while (_EMBEDDING_CACHE_BY_PROJECT.size >= 20) {
106
+ let oldestProject = null;
107
+ let oldestTime = Infinity;
108
+ for (const [p, t] of _EMBEDDING_CACHE_ACCESS_TIMES) {
109
+ if (t < oldestTime && p !== project) {
110
+ oldestTime = t;
111
+ oldestProject = p;
115
112
  }
116
- if (oldestProject) {
117
- _EMBEDDING_CACHE_BY_PROJECT.delete(oldestProject);
118
- _EMBEDDING_CACHE_ACCESS_TIMES.delete(oldestProject);
119
- __debugLog('[MCP DEBUG]', Date.now(), 'CACHE_PROJECT_EVICTED', { evictedProject: oldestProject, reason: 'max_projects_reached' });
120
- }
121
- } finally {
122
- _evictionInProgress = false;
113
+ }
114
+ if (oldestProject) {
115
+ _EMBEDDING_CACHE_BY_PROJECT.delete(oldestProject);
116
+ _EMBEDDING_CACHE_ACCESS_TIMES.delete(oldestProject);
117
+ __debugLog('[MCP DEBUG]', Date.now(), 'CACHE_PROJECT_EVICTED', { evictedProject: oldestProject, reason: 'max_projects_reached' });
118
+ } else {
119
+ // Safety: no evictable project found (all entries are current project?), break to avoid infinite loop
120
+ break;
123
121
  }
124
122
  }
125
123
  _EMBEDDING_CACHE_BY_PROJECT.set(project, new Map());
@@ -27,7 +27,10 @@ export class CheckSyncStatus {
27
27
  const driftReport = await watcherManager.checkSync();
28
28
  // build summary message
29
29
  let summary;
30
- if (driftReport.inSync) {
30
+ if (driftReport.indexingPending) {
31
+ summary = `Codebase indexing in progress — sync score not yet available. ${driftReport.totalFiles} files on disk awaiting indexing.`;
32
+ }
33
+ else if (driftReport.inSync) {
31
34
  summary = `Everything is in sync! ${driftReport.upToDate} files are up to date.`;
32
35
  }
33
36
  else {
@@ -45,7 +48,8 @@ export class CheckSyncStatus {
45
48
  }
46
49
  const result = {
47
50
  inSync: driftReport.inSync,
48
- syncScore: driftReport.syncScore,
51
+ syncScore: driftReport.indexingPending ? -1 : driftReport.syncScore,
52
+ indexingPending: !!driftReport.indexingPending,
49
53
  driftPercentage: driftReport.driftPercentage,
50
54
  summary,
51
55
  stats: {
@@ -70,10 +74,12 @@ export class CheckSyncStatus {
70
74
  contentMismatch: driftReport.contentMismatch
71
75
  };
72
76
  }
73
- // Update statusbar sync score live
74
- try {
75
- await watcherManager.writeSyncScore(driftReport.syncScore);
76
- } catch (e) { /* non-critical */ }
77
+ // Update statusbar sync score live (skip if indexing pending — don't write -1)
78
+ if (!driftReport.indexingPending) {
79
+ try {
80
+ await watcherManager.writeSyncScore(driftReport.syncScore);
81
+ } catch (e) { /* non-critical */ }
82
+ }
77
83
  logger.info({ inSync: driftReport.inSync, syncScore: driftReport.syncScore }, 'sync check complete');
78
84
  // Build human readable response
79
85
  const drifted = driftReport.missingFromMcp.length + driftReport.missingFromDisk.length + driftReport.contentMismatch.length;
@@ -99,7 +105,8 @@ export class CheckSyncStatus {
99
105
  if (more > 0) detailLines += `\n ... and ${more} more`;
100
106
  }
101
107
  }
102
- const message = `Sync Score: ${Math.round(driftReport.syncScore * 100)}%
108
+ const displayScore = driftReport.indexingPending ? 'Pending' : `${Math.round(driftReport.syncScore * 100)}%`;
109
+ const message = `Sync Score: ${displayScore}
103
110
  ${summary}
104
111
 
105
112
  Stats:
@@ -55,6 +55,11 @@ export class WatchForChangesNoCap {
55
55
  debounceCleanupTimer = null;
56
56
  // FIX 7.14: Track pending flush promises so stop() can await them
57
57
  pendingFlushPromises = new Set();
58
+ // PERF: Batch-level debounce — collect per-file handler results into batches
59
+ // so git operations changing many files don't fire hundreds of individual handler calls
60
+ _batchTimer = null;
61
+ _batchQueue = [];
62
+ _batchDebounceMs = 500; // collect events for 500ms before dispatching batch
58
63
  // stats tracking
59
64
  stats = {
60
65
  filesWatched: 0,
@@ -75,6 +80,37 @@ export class WatchForChangesNoCap {
75
80
  verbose: config.verbose ?? false
76
81
  };
77
82
  }
83
+ /**
84
+ * _enqueueBatchEvent - batch-level debounce for handler calls
85
+ *
86
+ * Instead of calling changeHandler() immediately per-file, queue events
87
+ * and dispatch the entire batch after 500ms of quiet. This prevents
88
+ * git operations (checkout, merge, rebase) from firing hundreds of
89
+ * individual handler calls that each trigger sync/DB work.
90
+ */
91
+ _enqueueBatchEvent(event) {
92
+ this._batchQueue.push(event);
93
+ if (this._batchTimer) clearTimeout(this._batchTimer);
94
+ this._batchTimer = setTimeout(async () => {
95
+ this._batchTimer = null;
96
+ const batch = this._batchQueue.splice(0);
97
+ if (batch.length === 0 || !this.changeHandler) return;
98
+ const batchSize = batch.length;
99
+ if (batchSize > 5) {
100
+ logger.info({ batchSize }, `batch debounce: dispatching ${batchSize} file events as batch`);
101
+ }
102
+ // Process each event in the batch sequentially
103
+ // (changeHandler expects individual events)
104
+ for (const evt of batch) {
105
+ try {
106
+ await this.changeHandler(evt);
107
+ }
108
+ catch (err) {
109
+ logger.error({ error: err, path: evt.path }, 'batch handler error for file');
110
+ }
111
+ }
112
+ }, this._batchDebounceMs);
113
+ }
78
114
  /**
79
115
  * startWatching - fires up the file watcher
80
116
  *
@@ -189,7 +225,7 @@ export class WatchForChangesNoCap {
189
225
  depth: undefined, // watch all depths
190
226
  // debouncing built into chokidar
191
227
  awaitWriteFinish: {
192
- stabilityThreshold: 300, // wait 300ms for file to stop changing
228
+ stabilityThreshold: 500, // wait 500ms for file to stop changing (reduced CPU from rapid fire events)
193
229
  pollInterval: 100 // check every 100ms
194
230
  },
195
231
  // dont follow symlinks (security)
@@ -240,6 +276,18 @@ export class WatchForChangesNoCap {
240
276
  await Promise.allSettled([...this.pendingFlushPromises]);
241
277
  this.pendingFlushPromises.clear();
242
278
  }
279
+ // PERF: Clear batch timer and flush pending batch events
280
+ if (this._batchTimer) {
281
+ clearTimeout(this._batchTimer);
282
+ this._batchTimer = null;
283
+ }
284
+ if (this._pendingBatchEvents.length > 0 && this.changeHandler) {
285
+ // Flush remaining batch events before shutdown
286
+ const batch = this._pendingBatchEvents.splice(0);
287
+ for (const evt of batch) {
288
+ try { await this.changeHandler(evt); } catch { /* shutting down */ }
289
+ }
290
+ }
243
291
  // FIX MED-13: Cancel all debounced handlers before clearing to prevent memory leaks
244
292
  // The debounce library's clear() method cancels pending timer execution
245
293
  for (const handler of this.debouncedHandlers.values()) {
@@ -381,7 +429,9 @@ export class WatchForChangesNoCap {
381
429
  if (latestEvent) {
382
430
  // Update timestamp to reflect when we actually process the event
383
431
  latestEvent.timestamp = new Date();
384
- await this.changeHandler(latestEvent);
432
+ // PERF: Route through batch debounce instead of calling handler directly
433
+ // This prevents git operations (200+ files) from firing 200 individual handler calls
434
+ this._enqueueBatchEvent(latestEvent);
385
435
  this.stats.eventsProcessed++;
386
436
  this.stats.lastEventTime = new Date();
387
437
  }
@@ -435,7 +485,8 @@ export class WatchForChangesNoCap {
435
485
  const flushPromise = Promise.resolve().then(async () => {
436
486
  try {
437
487
  latestEvent.timestamp = new Date();
438
- await this.changeHandler(latestEvent);
488
+ // PERF: Route through batch debounce
489
+ this._enqueueBatchEvent(latestEvent);
439
490
  this.stats.eventsProcessed++;
440
491
  this.stats.lastEventTime = new Date();
441
492
  }
@@ -479,23 +530,9 @@ export class WatchForChangesNoCap {
479
530
  const latestEvent = this.pendingEventData.get(key);
480
531
  if (latestEvent && this.changeHandler) {
481
532
  this.pendingEventData.delete(key);
482
- // FIX 7.14: Track flush promise so stop() can await it
483
- const flushPromise = Promise.resolve().then(async () => {
484
- try {
485
- latestEvent.timestamp = new Date();
486
- await this.changeHandler(latestEvent);
487
- this.stats.eventsProcessed++;
488
- this.stats.lastEventTime = new Date();
489
- }
490
- catch (error) {
491
- this.stats.errors++;
492
- logger.error({ error, event: latestEvent }, 'error processing stale debounce entry');
493
- }
494
- finally {
495
- this.pendingFlushPromises.delete(flushPromise);
496
- }
497
- });
498
- this.pendingFlushPromises.add(flushPromise);
533
+ // PERF: Route stale entries through batch debounce too
534
+ latestEvent.timestamp = new Date();
535
+ this._enqueueBatchEvent(latestEvent);
499
536
  }
500
537
  else {
501
538
  this.pendingEventData.delete(key);
@@ -29,6 +29,7 @@ export class WatcherManager {
29
29
  syncInterval = null;
30
30
  syncInProgress = false;
31
31
  syncTimeout = null;
32
+ lastLowScoreResyncAt = 0;
32
33
  constructor(config) {
33
34
  // Create handler first - it's the core component
34
35
  this.handler = new AutoUpdateTheMemories(config.handler);
@@ -114,6 +115,29 @@ export class WatcherManager {
114
115
  try {
115
116
  const report = await this.syncChecker.checkSync();
116
117
  await this.writeSyncScore(report.syncScore);
118
+ // Guard: indexingPending (score < 0) — nothing to do yet
119
+ if (report.syncScore < 0) {
120
+ logger.debug('periodic sync: indexing pending, skipping resync');
121
+ return;
122
+ }
123
+ // Low-score trigger: if score drops below threshold, force resync w/ debounce
124
+ const LOW_SCORE_THRESHOLD = parseFloat(process.env['SPECMEM_LOW_SCORE_THRESHOLD'] || '0.85');
125
+ const LOW_SCORE_DEBOUNCE_MS = parseInt(process.env['SPECMEM_LOW_SCORE_DEBOUNCE_MS'] || String(15 * 60 * 1000), 10);
126
+ if (report.syncScore < LOW_SCORE_THRESHOLD) {
127
+ const now = Date.now();
128
+ const debounceRemaining = LOW_SCORE_DEBOUNCE_MS - (now - this.lastLowScoreResyncAt);
129
+ if (debounceRemaining > 0) {
130
+ logger.warn({ syncScore: report.syncScore, debounceRemainingSec: Math.round(debounceRemaining / 1000) }, 'sync score below threshold but debounce active — skipping forced resync');
131
+ } else {
132
+ this.lastLowScoreResyncAt = now;
133
+ logger.warn({ syncScore: report.syncScore, threshold: LOW_SCORE_THRESHOLD }, 'sync score below threshold — forcing resync');
134
+ const resyncResult = await this.syncChecker.resyncEverythingFrFr();
135
+ logger.info({ filesAdded: resyncResult.filesAdded, filesUpdated: resyncResult.filesUpdated, errors: resyncResult.errors.length }, 'low-score forced resync complete');
136
+ const postReport = await this.syncChecker.checkSync();
137
+ await this.writeSyncScore(postReport.syncScore);
138
+ }
139
+ return;
140
+ }
117
141
  // Skip resync if already at >=98% — chokidar handles incremental changes
118
142
  if (report.syncScore >= 0.98) {
119
143
  logger.debug({ syncScore: report.syncScore }, 'periodic sync: score >=98%, skipping resync');
@@ -198,6 +222,8 @@ export class WatcherManager {
198
222
  * writeSyncScore - writes sync score to statusbar state file for display
199
223
  */
200
224
  async writeSyncScore(syncScore) {
225
+ // Guard: indexingPending returns -1 — never write negative values to statusbar
226
+ if (syncScore < 0) return;
201
227
  try {
202
228
  const projectPath = process.env.SPECMEM_PROJECT_PATH || process.cwd();
203
229
  const statusbarPath = join(projectPath, 'specmem', 'sockets', 'statusbar-state.json');
@@ -119,22 +119,26 @@ export class AreWeStillInSync {
119
119
  const totalFiles = diskFiles.length;
120
120
  const totalMemories = mcpFiles.length;
121
121
  const totalDrift = missingFromMcp.length + missingFromDisk.length + contentMismatch.length;
122
+ // FIX: If codebase_files is empty but disk files exist, indexing hasn't completed yet.
123
+ // Don't report 0% sync — that's misleading. Report indexing-pending state instead.
124
+ const indexingPending = totalMemories === 0 && totalFiles > 0;
122
125
  // Sync score = what % of disk files are correctly synced in MCP
123
126
  // Deleted-from-disk files are cleanup work, not sync failures
124
127
  const totalItems = totalFiles || 1;
125
- const driftPercentage = totalItems > 0 ? (totalDrift / totalItems) * 100 : 0;
126
- const syncScore = totalItems > 0 ? upToDate / totalItems : 1;
128
+ const driftPercentage = indexingPending ? 0 : (totalItems > 0 ? (totalDrift / totalItems) * 100 : 0);
129
+ const syncScore = indexingPending ? -1 : (totalItems > 0 ? upToDate / totalItems : 1);
127
130
  const report = {
128
- inSync: totalDrift === 0,
131
+ inSync: indexingPending ? false : totalDrift === 0,
129
132
  lastChecked: new Date(),
130
133
  totalFiles,
131
134
  totalMemories,
132
- missingFromMcp,
135
+ missingFromMcp: indexingPending ? [] : missingFromMcp,
133
136
  missingFromDisk,
134
- contentMismatch,
135
- upToDate,
137
+ contentMismatch: indexingPending ? [] : contentMismatch,
138
+ upToDate: indexingPending ? 0 : upToDate,
136
139
  driftPercentage,
137
- syncScore
140
+ syncScore,
141
+ indexingPending
138
142
  };
139
143
  this.lastSyncCheck = report.lastChecked;
140
144
  this.lastCheckTime = startTime; // FIX 7.03: Record check time for mtime optimization
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specmem-hardwicksoftware",
3
- "version": "3.7.30",
3
+ "version": "3.7.32",
4
4
  "type": "module",
5
5
  "description": "Your Claude Code sessions don't have to start from scratch anymore — SpecMem gives your AI real memory. It won't forget your conversations, your code, or your architecture decisions between sessions. That's the whole point. Semantic code indexing that actually works: TypeScript, JavaScript, Python, Go, Rust, Java, Kotlin, C, C++, HTML and more. It doesn't just track functions — it gets classes, methods, fields, constants, enums, macros, imports, structs, the whole codebase graph. There's chat memory too, powered by pgvector embeddings. You've also got token compression, team coordination, multi-agent comms, and file watching built in. 74+ MCP tools. Runs on PostgreSQL + Docker. It's kind of a big deal. justcalljon.pro",
6
6
  "main": "dist/index.js",
@@ -947,9 +947,14 @@ function adjustPgAuth() {
947
947
  // Backup and modify
948
948
  run(`sudo cp ${pgHbaPath} ${pgHbaPath}.backup`);
949
949
 
950
- // Add password auth for our user
950
+ // Add password auth for our user — check if already exists to prevent duplicates
951
951
  const authLine = `host ${DB_CONFIG.name} ${DB_CONFIG.user} 127.0.0.1/32 md5`;
952
- run(`echo '${authLine}' | sudo tee -a ${pgHbaPath}`);
952
+ const alreadyExists = run(`sudo grep -qF '${authLine}' ${pgHbaPath}`, { silent: true });
953
+ if (!alreadyExists.success) {
954
+ run(`echo '${authLine}' | sudo tee -a ${pgHbaPath}`);
955
+ } else {
956
+ log.info('pg_hba.conf auth line already present, skipping');
957
+ }
953
958
 
954
959
  // Reload PostgreSQL
955
960
  run('sudo systemctl reload postgresql 2>/dev/null || sudo -u postgres pg_ctl reload');
@@ -8799,9 +8799,14 @@ CREATE INDEX IF NOT EXISTS idx_embedding_queue_project ON embedding_queue (proje
8799
8799
  ? path.join(specmemPkg, 'mcp-proxy.cjs')
8800
8800
  : path.join(specmemPkg, 'bootstrap.cjs');
8801
8801
  // Container mode: postgres via unix socket in specmem/run/, user=specmem, trust auth
8802
+ // Socket dir bind-mounted to /data/run in container — PG socket appears here after container starts
8802
8803
  // Legacy mode: postgres on localhost:5432, legacy credentials
8803
8804
  const isContainerMode = process.env.SPECMEM_CONTAINER_MODE === 'true';
8804
8805
  const runDir = path.join(projectPath, 'specmem', 'run');
8806
+ if (isContainerMode) {
8807
+ // Ensure socket directory exists on host — container bind-mounts dataDir:/data
8808
+ try { fs.mkdirSync(runDir, { recursive: true }); } catch (e) { /* may already exist */ }
8809
+ }
8805
8810
  const dbEnv = isContainerMode ? {
8806
8811
  SPECMEM_DB_HOST: runDir,
8807
8812
  SPECMEM_DB_PORT: "5432",
@@ -13,9 +13,10 @@
13
13
  "cpus": 8
14
14
  },
15
15
  "embedding": {
16
- "batchSize": 24,
16
+ "batchSize": 64,
17
17
  "maxConcurrent": 5,
18
- "timeout": 45000
18
+ "timeout": 45000,
19
+ "throttleDelayMs": 50
19
20
  },
20
21
  "watcher": {
21
22
  "debounceMs": 750,
@@ -33,13 +34,13 @@
33
34
  "maxChunks": 75
34
35
  },
35
36
  "resources": {
36
- "cpuMin": 20,
37
- "cpuMax": 40,
37
+ "cpuMin": 10,
38
+ "cpuMax": 45,
38
39
  "cpuCoreMin": 1,
39
40
  "cpuCoreMax": 4,
40
41
  "ramMinMb": 4000,
41
- "ramMaxMb": 11500,
42
- "updatedAt": "2026-02-21T22:33:46.533Z"
42
+ "ramMaxMb": 15000,
43
+ "updatedAt": "2026-02-24T19:22:11.693Z"
43
44
  },
44
45
  "resourcePool": {
45
46
  "embedding": {
@@ -79,5 +80,24 @@
79
80
  "description": "Adaptive batch sizing based on CPU/RAM"
80
81
  },
81
82
  "enabledAt": "2026-02-12T23:19:17.948Z"
83
+ },
84
+ "powerMode": {
85
+ "level": "high",
86
+ "description": "Max Performance - for 16GB+ RAM systems",
87
+ "lazyLoading": false,
88
+ "diskCache": false,
89
+ "diskCacheMaxMb": 0,
90
+ "aggressiveCleanup": false,
91
+ "idleUnloadSeconds": 0,
92
+ "batchSize": 32,
93
+ "throttleDelayMs": 50,
94
+ "setAt": "2026-02-24T11:57:23.121Z"
95
+ },
96
+ "heavyOps": {
97
+ "enabled": true,
98
+ "enabledAt": "2026-02-24T11:57:35.508Z",
99
+ "originalBatchSize": 32,
100
+ "batchSizeMultiplier": 2,
101
+ "throttleReduction": 0.2
82
102
  }
83
103
  }
@@ -1,6 +1,6 @@
1
1
  ; ============================================
2
2
  ; SPECMEM BRAIN CONTAINER - DYNAMIC SUPERVISORD CONFIG
3
- ; Generated by specmem-init at 2026-02-22T17:39:56.598Z
3
+ ; Generated by specmem-init at 2026-02-24T19:19:59.260Z
4
4
  ; Thread counts from model-config.json resourcePool
5
5
  ; ============================================
6
6
 
@@ -7,5 +7,17 @@
7
7
  "serviceMode": {
8
8
  "enabled": false,
9
9
  "disabledAt": "2026-02-18T21:38:50.526Z"
10
+ },
11
+ "powerMode": {
12
+ "level": "high",
13
+ "description": "Max Performance - for 16GB+ RAM systems",
14
+ "lazyLoading": false,
15
+ "diskCache": false,
16
+ "diskCacheMaxMb": 0,
17
+ "aggressiveCleanup": false,
18
+ "idleUnloadSeconds": 0,
19
+ "batchSize": 32,
20
+ "throttleDelayMs": 50,
21
+ "setAt": "2026-02-24T11:57:23.121Z"
10
22
  }
11
23
  }