specmem-hardwicksoftware 3.5.99 → 3.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/specmem-statusbar.cjs +154 -298
- package/claude-hooks/agent-loading-hook.js +8 -4
- package/claude-hooks/team-comms-enforcer.cjs +109 -92
- package/dist/config/embeddingTimeouts.js +4 -4
- package/dist/database.js +52 -6
- package/dist/db/bigBrainMigrations.js +7 -6
- package/dist/db/memoryDrilldown.sql +1 -1
- package/dist/db/projectSchemaInit.sql +21 -0
- package/dist/index.js +238 -13
- package/dist/installer/firstRun.js +2 -2
- package/dist/mcp/embeddingServerManager.js +225 -7
- package/dist/mcp/healthMonitor.js +165 -32
- package/dist/mcp/tools/embeddingControl.js +31 -0
- package/dist/mcp/tools/teamComms.js +16 -0
- package/dist/mcp/watcherIntegration.js +50 -7
- package/dist/services/CameraZoomSearch.js +62 -5
- package/dist/services/DimensionService.js +73 -6
- package/dist/services/EmbeddingQueue.js +64 -0
- package/dist/services/MemoryDrilldown.js +19 -12
- package/dist/tools/goofy/findCodePointers.js +11 -7
- package/dist/tools/goofy/findWhatISaid.js +145 -53
- package/dist/utils/qoms.js +187 -4
- package/dist/watcher/changeHandler.js +54 -4
- package/dist/watcher/fileWatcher.js +121 -1
- package/dist/watcher/index.js +75 -31
- package/dist/watcher/syncChecker.js +248 -63
- package/embedding-sandbox/__pycache__/frankenstein-embeddings.cpython-313.pyc +0 -0
- package/embedding-sandbox/frankenstein-embeddings.py +175 -64
- package/package.json +1 -1
|
@@ -44,6 +44,13 @@ export class WatchForChangesNoCap {
|
|
|
44
44
|
debouncedHandlers = new Map();
|
|
45
45
|
// FIX MED-14 & LOW-15: Track latest event data per key to avoid stale closures
|
|
46
46
|
pendingEventData = new Map();
|
|
47
|
+
// FIX Issue #13: Track when each debounce entry was created for age-based cleanup
|
|
48
|
+
debounceEntryTimestamps = new Map();
|
|
49
|
+
// FIX Issue #13: Configurable limits for debounce map growth
|
|
50
|
+
debounceMapMaxSize = parseInt(process.env['SPECMEM_DEBOUNCE_MAP_MAX_SIZE'] || '5000');
|
|
51
|
+
debounceCleanupIntervalMs = parseInt(process.env['SPECMEM_DEBOUNCE_CLEANUP_INTERVAL_MS'] || '300000'); // 5 min
|
|
52
|
+
debounceMaxAgeMs = parseInt(process.env['SPECMEM_DEBOUNCE_MAX_AGE_MS'] || '60000'); // 60s
|
|
53
|
+
debounceCleanupTimer = null;
|
|
47
54
|
// stats tracking
|
|
48
55
|
stats = {
|
|
49
56
|
filesWatched: 0,
|
|
@@ -130,6 +137,11 @@ export class WatchForChangesNoCap {
|
|
|
130
137
|
'**/memory-dumps/**',
|
|
131
138
|
'**/tmp/**',
|
|
132
139
|
'**/temp/**',
|
|
140
|
+
// SpecMem runtime directories - NEVER watch these
|
|
141
|
+
// statusbar-state.json changes every few seconds causing infinite feedback loops
|
|
142
|
+
'**/specmem/sockets/**',
|
|
143
|
+
'**/specmem/run/**',
|
|
144
|
+
'**/.specmem/**',
|
|
133
145
|
// Test artifacts
|
|
134
146
|
'**/coverage/**',
|
|
135
147
|
'**/__pycache__/**',
|
|
@@ -185,6 +197,10 @@ export class WatchForChangesNoCap {
|
|
|
185
197
|
this.setupEventHandlers();
|
|
186
198
|
this.isWatching = true;
|
|
187
199
|
this.restartCount = 0;
|
|
200
|
+
// FIX Issue #13: Start periodic cleanup of stale debounce entries
|
|
201
|
+
this.debounceCleanupTimer = setInterval(() => {
|
|
202
|
+
this.cleanupStaleDebounceEntries();
|
|
203
|
+
}, this.debounceCleanupIntervalMs);
|
|
188
204
|
logger.info('file watcher started successfully - were LIVE');
|
|
189
205
|
}
|
|
190
206
|
catch (error) {
|
|
@@ -205,6 +221,11 @@ export class WatchForChangesNoCap {
|
|
|
205
221
|
await this.watcher.close();
|
|
206
222
|
this.watcher = null;
|
|
207
223
|
}
|
|
224
|
+
// FIX Issue #13: Clear debounce cleanup timer
|
|
225
|
+
if (this.debounceCleanupTimer) {
|
|
226
|
+
clearInterval(this.debounceCleanupTimer);
|
|
227
|
+
this.debounceCleanupTimer = null;
|
|
228
|
+
}
|
|
208
229
|
// FIX MED-13: Cancel all debounced handlers before clearing to prevent memory leaks
|
|
209
230
|
// The debounce library's clear() method cancels pending timer execution
|
|
210
231
|
for (const handler of this.debouncedHandlers.values()) {
|
|
@@ -212,6 +233,7 @@ export class WatchForChangesNoCap {
|
|
|
212
233
|
}
|
|
213
234
|
this.debouncedHandlers.clear();
|
|
214
235
|
this.pendingEventData.clear();
|
|
236
|
+
this.debounceEntryTimestamps.clear();
|
|
215
237
|
this.isWatching = false;
|
|
216
238
|
logger.info({ stats: this.stats }, 'file watcher stopped - peace out');
|
|
217
239
|
}
|
|
@@ -319,8 +341,14 @@ export class WatchForChangesNoCap {
|
|
|
319
341
|
// already debouncing this file - the handler will use updated pendingEventData
|
|
320
342
|
// FIX MED-14: Don't return early - we've already updated the event data above
|
|
321
343
|
// The debounced function will pick up the latest event when it fires
|
|
344
|
+
// Update the timestamp for the existing entry
|
|
345
|
+
this.debounceEntryTimestamps.set(key, Date.now());
|
|
322
346
|
return;
|
|
323
347
|
}
|
|
348
|
+
// FIX Issue #13: If debounce map exceeds max size, flush oldest entries immediately
|
|
349
|
+
if (this.debouncedHandlers.size >= this.debounceMapMaxSize) {
|
|
350
|
+
this.flushOldestDebounceEntries();
|
|
351
|
+
}
|
|
324
352
|
// create debounced handler that reads latest event data when executing
|
|
325
353
|
const debouncedHandler = debounce(async () => {
|
|
326
354
|
try {
|
|
@@ -342,17 +370,109 @@ export class WatchForChangesNoCap {
|
|
|
342
370
|
logger.error({ error, event: latestEvent }, 'error processing file change');
|
|
343
371
|
}
|
|
344
372
|
finally {
|
|
345
|
-
// remove from debounce map
|
|
373
|
+
// remove from debounce map, pending data, and timestamps
|
|
346
374
|
this.debouncedHandlers.delete(key);
|
|
347
375
|
this.pendingEventData.delete(key);
|
|
376
|
+
this.debounceEntryTimestamps.delete(key);
|
|
348
377
|
}
|
|
349
378
|
}, this.config.debounceMs);
|
|
350
379
|
this.debouncedHandlers.set(key, debouncedHandler);
|
|
380
|
+
// FIX Issue #13: Track when this debounce entry was created
|
|
381
|
+
this.debounceEntryTimestamps.set(key, Date.now());
|
|
351
382
|
debouncedHandler();
|
|
352
383
|
if (this.config.verbose) {
|
|
353
384
|
logger.debug({ event }, 'file change detected');
|
|
354
385
|
}
|
|
355
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* FIX Issue #13: Flush the oldest debounce entries immediately when map exceeds max size.
|
|
389
|
+
* Fires the debounced handlers immediately (processes them without waiting for debounce timer).
|
|
390
|
+
*/
|
|
391
|
+
flushOldestDebounceEntries() {
|
|
392
|
+
// Sort entries by timestamp (oldest first) and flush 10% of the map
|
|
393
|
+
const entries = [...this.debounceEntryTimestamps.entries()]
|
|
394
|
+
.sort((a, b) => a[1] - b[1]);
|
|
395
|
+
const flushCount = Math.max(1, Math.ceil(entries.length * 0.1));
|
|
396
|
+
let flushedCount = 0;
|
|
397
|
+
for (let i = 0; i < flushCount && i < entries.length; i++) {
|
|
398
|
+
const [key] = entries[i];
|
|
399
|
+
const handler = this.debouncedHandlers.get(key);
|
|
400
|
+
if (handler) {
|
|
401
|
+
// Cancel the debounce timer and fire immediately
|
|
402
|
+
handler.clear();
|
|
403
|
+
this.debouncedHandlers.delete(key);
|
|
404
|
+
this.debounceEntryTimestamps.delete(key);
|
|
405
|
+
// Fire the handler immediately with the pending event data
|
|
406
|
+
const latestEvent = this.pendingEventData.get(key);
|
|
407
|
+
if (latestEvent && this.changeHandler) {
|
|
408
|
+
this.pendingEventData.delete(key);
|
|
409
|
+
// Fire and forget - don't await, just dispatch
|
|
410
|
+
Promise.resolve().then(async () => {
|
|
411
|
+
try {
|
|
412
|
+
latestEvent.timestamp = new Date();
|
|
413
|
+
await this.changeHandler(latestEvent);
|
|
414
|
+
this.stats.eventsProcessed++;
|
|
415
|
+
this.stats.lastEventTime = new Date();
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
this.stats.errors++;
|
|
419
|
+
logger.error({ error, event: latestEvent }, 'error processing flushed debounce entry');
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
this.pendingEventData.delete(key);
|
|
425
|
+
}
|
|
426
|
+
flushedCount++;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (flushedCount > 0) {
|
|
430
|
+
logger.warn({ flushedCount, mapSize: this.debouncedHandlers.size, maxSize: this.debounceMapMaxSize }, 'Flushed oldest debounce entries due to map size limit');
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* FIX Issue #13: Periodic cleanup of stale debounce entries older than debounceMaxAgeMs.
|
|
435
|
+
* Prevents unbounded growth when debounce timers keep getting reset.
|
|
436
|
+
*/
|
|
437
|
+
cleanupStaleDebounceEntries() {
|
|
438
|
+
const now = Date.now();
|
|
439
|
+
let cleanedCount = 0;
|
|
440
|
+
for (const [key, timestamp] of this.debounceEntryTimestamps.entries()) {
|
|
441
|
+
if (now - timestamp > this.debounceMaxAgeMs) {
|
|
442
|
+
const handler = this.debouncedHandlers.get(key);
|
|
443
|
+
if (handler) {
|
|
444
|
+
// Cancel the debounce timer and fire immediately
|
|
445
|
+
handler.clear();
|
|
446
|
+
this.debouncedHandlers.delete(key);
|
|
447
|
+
// Fire the handler immediately with the pending event data
|
|
448
|
+
const latestEvent = this.pendingEventData.get(key);
|
|
449
|
+
if (latestEvent && this.changeHandler) {
|
|
450
|
+
this.pendingEventData.delete(key);
|
|
451
|
+
Promise.resolve().then(async () => {
|
|
452
|
+
try {
|
|
453
|
+
latestEvent.timestamp = new Date();
|
|
454
|
+
await this.changeHandler(latestEvent);
|
|
455
|
+
this.stats.eventsProcessed++;
|
|
456
|
+
this.stats.lastEventTime = new Date();
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
this.stats.errors++;
|
|
460
|
+
logger.error({ error, event: latestEvent }, 'error processing stale debounce entry');
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
this.pendingEventData.delete(key);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
this.debounceEntryTimestamps.delete(key);
|
|
469
|
+
cleanedCount++;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (cleanedCount > 0) {
|
|
473
|
+
logger.warn({ cleanedCount, remainingEntries: this.debouncedHandlers.size }, 'Cleaned up stale debounce entries');
|
|
474
|
+
}
|
|
475
|
+
}
|
|
356
476
|
/**
|
|
357
477
|
* skipTheBoringShit - checks if file should be ignored
|
|
358
478
|
*
|
package/dist/watcher/index.js
CHANGED
|
@@ -27,6 +27,8 @@ export class WatcherManager {
|
|
|
27
27
|
syncChecker;
|
|
28
28
|
isRunning = false;
|
|
29
29
|
syncInterval = null;
|
|
30
|
+
syncInProgress = false;
|
|
31
|
+
syncTimeout = null;
|
|
30
32
|
constructor(config) {
|
|
31
33
|
// Create handler first - it's the core component
|
|
32
34
|
this.handler = new AutoUpdateTheMemories(config.handler);
|
|
@@ -59,36 +61,13 @@ export class WatcherManager {
|
|
|
59
61
|
});
|
|
60
62
|
// Mark as running IMMEDIATELY - don't wait for sync
|
|
61
63
|
this.isRunning = true;
|
|
62
|
-
// 3. start periodic sync checking
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
driftPercentage: report.driftPercentage,
|
|
70
|
-
missingFromMcp: report.missingFromMcp.length,
|
|
71
|
-
contentMismatch: report.contentMismatch.length
|
|
72
|
-
}, 'drift detected during periodic check');
|
|
73
|
-
// Auto-resync when drift is detected
|
|
74
|
-
if (report.missingFromMcp.length > 0 || report.contentMismatch.length > 0) {
|
|
75
|
-
logger.info('periodic check triggering auto-resync...');
|
|
76
|
-
const resyncResult = await this.syncChecker.resyncEverythingFrFr();
|
|
77
|
-
logger.info({
|
|
78
|
-
filesAdded: resyncResult.filesAdded,
|
|
79
|
-
filesUpdated: resyncResult.filesUpdated,
|
|
80
|
-
errors: resyncResult.errors.length
|
|
81
|
-
}, 'periodic auto-resync complete');
|
|
82
|
-
// Update score after resync
|
|
83
|
-
const postReport = await this.syncChecker.checkSync();
|
|
84
|
-
await this.writeSyncScore(postReport.syncScore);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
logger.error({ error }, 'periodic sync check failed');
|
|
90
|
-
}
|
|
91
|
-
}, syncCheckIntervalMinutes * 60 * 1000);
|
|
64
|
+
// 3. start periodic sync checking using setTimeout + recursive scheduling
|
|
65
|
+
// This prevents sync interval stacking (Issue #2) - if a sync takes longer
|
|
66
|
+
// than the interval, the next one won't start until the previous completes.
|
|
67
|
+
// Interval is configurable via SPECMEM_SYNC_CHECK_INTERVAL_MS env var.
|
|
68
|
+
const syncIntervalMs = parseInt(process.env['SPECMEM_SYNC_CHECK_INTERVAL_MS'] || String(syncCheckIntervalMinutes * 60 * 1000), 10);
|
|
69
|
+
logger.info({ syncIntervalMs }, 'scheduling periodic sync checks with recursive setTimeout');
|
|
70
|
+
this.scheduleSyncCheck(syncIntervalMs);
|
|
92
71
|
logger.info('watcher manager started - ready for changes');
|
|
93
72
|
// 4. BACKGROUND sync check - does NOT block startup
|
|
94
73
|
// Runs the full filesystem scan in background so becomes responsive immediately
|
|
@@ -104,6 +83,67 @@ export class WatcherManager {
|
|
|
104
83
|
throw error;
|
|
105
84
|
}
|
|
106
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* scheduleSyncCheck - schedules the next sync check using setTimeout
|
|
88
|
+
* Uses recursive scheduling instead of setInterval to prevent stacking (Issue #2).
|
|
89
|
+
* The next check is only scheduled AFTER the current one completes.
|
|
90
|
+
*/
|
|
91
|
+
scheduleSyncCheck(intervalMs) {
|
|
92
|
+
if (!this.isRunning) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.syncTimeout = setTimeout(async () => {
|
|
96
|
+
await this.runPeriodicSync();
|
|
97
|
+
// Schedule next check only after current one completes - prevents stacking
|
|
98
|
+
this.scheduleSyncCheck(intervalMs);
|
|
99
|
+
}, intervalMs);
|
|
100
|
+
// Allow process to exit even if timeout is pending
|
|
101
|
+
if (this.syncTimeout && typeof this.syncTimeout.unref === 'function') {
|
|
102
|
+
this.syncTimeout.unref();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* runPeriodicSync - executes a single periodic sync check with guard
|
|
107
|
+
* Uses syncInProgress flag to prevent concurrent sync operations (Issue #2).
|
|
108
|
+
* If a sync is already running, logs a skip and returns immediately.
|
|
109
|
+
*/
|
|
110
|
+
async runPeriodicSync() {
|
|
111
|
+
if (this.syncInProgress) {
|
|
112
|
+
logger.warn('periodic sync check skipped - another sync is already in progress (preventing stacking)');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.syncInProgress = true;
|
|
116
|
+
try {
|
|
117
|
+
const report = await this.syncChecker.checkSync();
|
|
118
|
+
await this.writeSyncScore(report.syncScore);
|
|
119
|
+
if (!report.inSync) {
|
|
120
|
+
logger.warn({
|
|
121
|
+
driftPercentage: report.driftPercentage,
|
|
122
|
+
missingFromMcp: report.missingFromMcp.length,
|
|
123
|
+
contentMismatch: report.contentMismatch.length
|
|
124
|
+
}, 'drift detected during periodic check');
|
|
125
|
+
// Auto-resync when drift is detected
|
|
126
|
+
if (report.missingFromMcp.length > 0 || report.contentMismatch.length > 0) {
|
|
127
|
+
logger.info('periodic check triggering auto-resync...');
|
|
128
|
+
const resyncResult = await this.syncChecker.resyncEverythingFrFr();
|
|
129
|
+
logger.info({
|
|
130
|
+
filesAdded: resyncResult.filesAdded,
|
|
131
|
+
filesUpdated: resyncResult.filesUpdated,
|
|
132
|
+
errors: resyncResult.errors.length
|
|
133
|
+
}, 'periodic auto-resync complete');
|
|
134
|
+
// Update score after resync
|
|
135
|
+
const postReport = await this.syncChecker.checkSync();
|
|
136
|
+
await this.writeSyncScore(postReport.syncScore);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
logger.error({ error }, 'periodic sync check failed');
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
this.syncInProgress = false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
107
147
|
/**
|
|
108
148
|
* runBackgroundSyncCheck - runs sync check in background without blocking
|
|
109
149
|
*
|
|
@@ -169,11 +209,15 @@ export class WatcherManager {
|
|
|
169
209
|
return;
|
|
170
210
|
}
|
|
171
211
|
logger.info('stopping watcher manager...');
|
|
172
|
-
// stop sync checking
|
|
212
|
+
// stop sync checking (clear both legacy interval and new timeout)
|
|
173
213
|
if (this.syncInterval) {
|
|
174
214
|
clearInterval(this.syncInterval);
|
|
175
215
|
this.syncInterval = null;
|
|
176
216
|
}
|
|
217
|
+
if (this.syncTimeout) {
|
|
218
|
+
clearTimeout(this.syncTimeout);
|
|
219
|
+
this.syncTimeout = null;
|
|
220
|
+
}
|
|
177
221
|
// stop file watcher
|
|
178
222
|
await this.watcher.stopWatching();
|
|
179
223
|
// stop queue processing
|