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.
@@ -48,7 +48,8 @@ try {
48
48
  // CONFIGURATION
49
49
  // ============================================================================
50
50
  const MAX_SEARCHES_BEFORE_BLOCK = 3;
51
- const BROADCAST_CHECK_INTERVAL = 5; // Check broadcasts every 5 tool usages
51
+ const TEAM_COMMS_CHECK_INTERVAL = 4; // MUST read_team_messages every 4 tool usages
52
+ const BROADCAST_CHECK_INTERVAL = 5; // MUST read_team_messages w/ include_broadcasts every 5 tool usages
52
53
  const HELP_CHECK_INTERVAL = 8; // Check help requests every 8 tool usages
53
54
 
54
55
  // Tools that count as "announcing"
@@ -142,11 +143,14 @@ function getAgentState(tracking, sessionId) {
142
143
  usedMemoryTools: false,
143
144
  searchCount: 0,
144
145
  blockedCount: 0,
145
- toolUsageCount: 0, // Total tool calls since last broadcast check
146
- helpToolUsageCount: 0, // Total tool calls since last help check
146
+ commsToolCount: 0, // Tool calls since last team comms check (every 4)
147
+ broadcastToolCount: 0, // Tool calls since last broadcast check (every 5)
148
+ helpToolUsageCount: 0, // Tool calls since last help check (every 8)
149
+ lastCommsCheck: Date.now(),
147
150
  lastBroadcastCheck: Date.now(),
148
151
  lastHelpCheck: Date.now(),
149
- needsBroadcastCheck: false, // Flag when they hit the limit
152
+ needsCommsCheck: false, // HARD BLOCK until they read team messages
153
+ needsBroadcastCheck: false, // HARD BLOCK until they read broadcasts
150
154
  needsHelpCheck: false, // Flag when they hit the limit
151
155
  lastActivity: Date.now()
152
156
  };
@@ -287,93 +291,78 @@ process.stdin.on('end', () => {
287
291
  state.lastActivity = Date.now();
288
292
 
289
293
  // ========================================================================
290
- // ALWAYS ALLOWED TOOLS - but track state
294
+ // TRACK STATE FOR ALL TOOLS (announcements, claims, memory, comms)
291
295
  // ========================================================================
292
- if (ALWAYS_ALLOWED.includes(toolName)) {
293
- // Track announcements
294
- if (ANNOUNCE_TOOLS.includes(toolName)) {
295
- state.announced = true;
296
- }
297
- // Track claims + write to shared claims file
298
- if (CLAIM_TOOLS.includes(toolName)) {
299
- state.claimed = true;
300
- // Write claim to shared file so other agents can see it
301
- const params = data.tool_input || {};
302
- const claimFiles = params.files || [];
303
- const claimDesc = params.description || 'unnamed task';
304
- const claimsFile = `${PROJECT_TMP_DIR}/active-claims.json`;
305
- try {
306
- let claims = {};
307
- if (fs.existsSync(claimsFile)) {
308
- claims = JSON.parse(fs.readFileSync(claimsFile, 'utf8'));
309
- }
310
- // Add this claim
311
- const claimId = `claim-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
312
- claims[claimId] = {
313
- sessionId,
314
- agentId: sessionId,
315
- files: claimFiles,
316
- description: claimDesc,
317
- createdAt: Date.now()
318
- };
319
- // Clean up old claims (>30 min)
320
- const now = Date.now();
296
+ if (ANNOUNCE_TOOLS.includes(toolName)) {
297
+ state.announced = true;
298
+ }
299
+ if (CLAIM_TOOLS.includes(toolName)) {
300
+ state.claimed = true;
301
+ const params = data.tool_input || {};
302
+ const claimFiles = params.files || [];
303
+ const claimDesc = params.description || 'unnamed task';
304
+ const claimsFile = `${PROJECT_TMP_DIR}/active-claims.json`;
305
+ try {
306
+ let claims = {};
307
+ if (fs.existsSync(claimsFile)) {
308
+ claims = JSON.parse(fs.readFileSync(claimsFile, 'utf8'));
309
+ }
310
+ const claimId = `claim-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
311
+ claims[claimId] = {
312
+ sessionId, agentId: sessionId, files: claimFiles,
313
+ description: claimDesc, createdAt: Date.now()
314
+ };
315
+ const now = Date.now();
316
+ for (const [id, claim] of Object.entries(claims)) {
317
+ if (now - claim.createdAt > 1800000) delete claims[id];
318
+ }
319
+ fs.writeFileSync(claimsFile, JSON.stringify(claims, null, 2));
320
+ state.currentClaimId = claimId;
321
+ } catch (e) {}
322
+ }
323
+ if (toolName === 'mcp__specmem__release_task') {
324
+ const claimsFile = `${PROJECT_TMP_DIR}/active-claims.json`;
325
+ try {
326
+ if (fs.existsSync(claimsFile)) {
327
+ let claims = JSON.parse(fs.readFileSync(claimsFile, 'utf8'));
321
328
  for (const [id, claim] of Object.entries(claims)) {
322
- if (now - claim.createdAt > 1800000) delete claims[id];
329
+ if (claim.sessionId === sessionId) delete claims[id];
323
330
  }
324
331
  fs.writeFileSync(claimsFile, JSON.stringify(claims, null, 2));
325
- state.currentClaimId = claimId;
326
- } catch (e) {}
327
- }
328
- // Track release_task - remove from shared claims
329
- if (toolName === 'mcp__specmem__release_task') {
330
- const claimsFile = `${PROJECT_TMP_DIR}/active-claims.json`;
331
- try {
332
- if (fs.existsSync(claimsFile)) {
333
- let claims = JSON.parse(fs.readFileSync(claimsFile, 'utf8'));
334
- // Remove claims from this session
335
- for (const [id, claim] of Object.entries(claims)) {
336
- if (claim.sessionId === sessionId) delete claims[id];
337
- }
338
- fs.writeFileSync(claimsFile, JSON.stringify(claims, null, 2));
339
- }
340
- } catch (e) {}
341
- state.claimed = false;
342
- state.editedFiles = [];
343
- }
344
- // Track memory tool usage
345
- if (MEMORY_TOOLS.includes(toolName)) {
346
- state.usedMemoryTools = true;
347
- state.searchCount = 0; // Reset search count
348
- }
349
- // Track broadcast checks (read_team_messages with broadcasts)
350
- if (BROADCAST_CHECK_TOOLS.includes(toolName)) {
351
- // Check if they included broadcasts in the params
352
- const params = data.tool_input || {};
353
- if (params.include_broadcasts !== false) { // Default is true
354
- state.toolUsageCount = 0; // Reset counter
355
- state.lastBroadcastCheck = Date.now();
356
- state.needsBroadcastCheck = false;
357
332
  }
333
+ } catch (e) {}
334
+ state.claimed = false;
335
+ state.editedFiles = [];
336
+ }
337
+ if (MEMORY_TOOLS.includes(toolName)) {
338
+ state.usedMemoryTools = true;
339
+ state.searchCount = 0;
340
+ }
341
+ // Track team comms reads - resets comms counter
342
+ if (BROADCAST_CHECK_TOOLS.includes(toolName)) {
343
+ state.commsToolCount = 0;
344
+ state.lastCommsCheck = Date.now();
345
+ state.needsCommsCheck = false;
346
+ // Also reset broadcast counter IF they included broadcasts
347
+ const params = data.tool_input || {};
348
+ if (params.include_broadcasts !== false) {
349
+ state.broadcastToolCount = 0;
350
+ state.lastBroadcastCheck = Date.now();
351
+ state.needsBroadcastCheck = false;
358
352
  }
359
- // Track help checks
360
- if (HELP_CHECK_TOOLS.includes(toolName)) {
361
- state.helpToolUsageCount = 0; // Reset counter
362
- state.lastHelpCheck = Date.now();
363
- state.needsHelpCheck = false;
364
- }
365
- saveTracking(tracking);
366
- console.log(JSON.stringify({ continue: true }));
367
- return;
353
+ }
354
+ if (HELP_CHECK_TOOLS.includes(toolName)) {
355
+ state.helpToolUsageCount = 0;
356
+ state.lastHelpCheck = Date.now();
357
+ state.needsHelpCheck = false;
368
358
  }
369
359
 
370
360
  // ========================================================================
371
- // CHECK: Must announce first
361
+ // CHECK: Must announce first (before anything else)
372
362
  // ========================================================================
373
- if (!state.announced) {
363
+ if (!state.announced && !ALWAYS_ALLOWED.includes(toolName)) {
374
364
  state.blockedCount++;
375
365
  saveTracking(tracking);
376
- // Make the announcement requirement very clear with proper channel guidance
377
366
  console.log(blockResponse(
378
367
  `[BLOCKED] You MUST ANNOUNCE yourself first!\n\n` +
379
368
  `This is your MANDATORY FIRST ACTION before any other tool:\n\n` +
@@ -386,23 +375,43 @@ process.stdin.on('end', () => {
386
375
  }
387
376
 
388
377
  // ========================================================================
389
- // INCREMENT TOOL USAGE COUNTERS (for broadcast/help enforcement)
378
+ // INCREMENT ALL COUNTERS ON EVERY TOOL CALL (per-agent, per-session)
379
+ // This counts ALL tools including ALWAYS_ALLOWED - no dodging
390
380
  // ========================================================================
391
- state.toolUsageCount = (state.toolUsageCount || 0) + 1;
381
+ state.commsToolCount = (state.commsToolCount || 0) + 1;
382
+ state.broadcastToolCount = (state.broadcastToolCount || 0) + 1;
392
383
  state.helpToolUsageCount = (state.helpToolUsageCount || 0) + 1;
393
384
 
394
385
  // ========================================================================
395
- // CHECK: Must check broadcasts every 5 tool usages
386
+ // HARD BLOCK: Must read team messages every 4 tool usages
387
+ // read_team_messages() satisfies this - any mode
388
+ // ========================================================================
389
+ if (state.commsToolCount >= TEAM_COMMS_CHECK_INTERVAL && !BROADCAST_CHECK_TOOLS.includes(toolName)) {
390
+ state.needsCommsCheck = true;
391
+ state.blockedCount++;
392
+ saveTracking(tracking);
393
+ console.log(blockResponse(
394
+ `[BLOCKED] MANDATORY team comms check! (${state.commsToolCount} tools since last check)\n\n` +
395
+ `REQUIRED: read_team_messages({include_swarms: true, limit: 5})\n\n` +
396
+ `You MUST check team messages every 4 tool calls. This is non-negotiable.\n` +
397
+ `Other agents may have critical updates for you. CHECK NOW.`
398
+ ));
399
+ return;
400
+ }
401
+
396
402
  // ========================================================================
397
- if (state.toolUsageCount >= BROADCAST_CHECK_INTERVAL) {
403
+ // HARD BLOCK: Must read broadcasts every 5 tool usages
404
+ // read_team_messages({include_broadcasts: true}) satisfies this
405
+ // ========================================================================
406
+ if (state.broadcastToolCount >= BROADCAST_CHECK_INTERVAL && !BROADCAST_CHECK_TOOLS.includes(toolName)) {
398
407
  state.needsBroadcastCheck = true;
399
408
  state.blockedCount++;
400
409
  saveTracking(tracking);
401
410
  console.log(blockResponse(
402
- `[BLOCKED] Time to check broadcasts! (${state.toolUsageCount} tools since last check)\n\n` +
403
- `REQUIRED: read_team_messages({include_broadcasts: true, limit: 10})\n\n` +
404
- `Stay informed about team updates. Other swarms might need your help!\n` +
405
- `After checking, you can continue working.`
411
+ `[BLOCKED] MANDATORY broadcast check! (${state.broadcastToolCount} tools since last broadcast check)\n\n` +
412
+ `REQUIRED: read_team_messages({include_broadcasts: true, include_swarms: true, limit: 10})\n\n` +
413
+ `You MUST check broadcasts every 5 tool calls. This is non-negotiable.\n` +
414
+ `Team-wide announcements and status updates require your attention. CHECK NOW.`
406
415
  ));
407
416
  return;
408
417
  }
@@ -410,7 +419,7 @@ process.stdin.on('end', () => {
410
419
  // ========================================================================
411
420
  // CHECK: Must check help requests every 8 tool usages
412
421
  // ========================================================================
413
- if (state.helpToolUsageCount >= HELP_CHECK_INTERVAL) {
422
+ if (state.helpToolUsageCount >= HELP_CHECK_INTERVAL && !HELP_CHECK_TOOLS.includes(toolName)) {
414
423
  state.needsHelpCheck = true;
415
424
  state.blockedCount++;
416
425
  saveTracking(tracking);
@@ -425,12 +434,20 @@ process.stdin.on('end', () => {
425
434
  }
426
435
 
427
436
  // ========================================================================
428
- // WARN: Approaching broadcast check
437
+ // ALWAYS ALLOWED TOOLS - pass through after counter checks
438
+ // ========================================================================
439
+ if (ALWAYS_ALLOWED.includes(toolName)) {
440
+ saveTracking(tracking);
441
+ console.log(JSON.stringify({ continue: true }));
442
+ return;
443
+ }
444
+
445
+ // ========================================================================
446
+ // WARN: Approaching comms check
429
447
  // ========================================================================
430
- if (state.toolUsageCount === BROADCAST_CHECK_INTERVAL - 1) {
431
- // Don't block, just warn
448
+ if (state.commsToolCount === TEAM_COMMS_CHECK_INTERVAL - 1) {
432
449
  console.log(allowWithReminder(
433
- `[HEADS UP] Next tool call will require broadcast check. Consider checking now with read_team_messages({include_broadcasts: true})`
450
+ `[HEADS UP] Next tool call will require team comms check. Do it now: read_team_messages({include_swarms: true, limit: 5})`
434
451
  ));
435
452
  // Don't return - continue to other checks
436
453
  }
@@ -89,10 +89,10 @@ export const embeddingTimeouts = {
89
89
  },
90
90
  /**
91
91
  * Timeout for embedding generation during memory search (find_memory)
92
- * Env: SPECMEM_FIND_EMBEDDING_TIMEOUT_MS (default: 60000 = 60s)
92
+ * Env: SPECMEM_FIND_EMBEDDING_TIMEOUT_MS (default: 120000 = 120s, increased from 60s to match cold-start)
93
93
  */
94
94
  get search() {
95
- return this.master ?? parseTimeoutMs('SPECMEM_FIND_EMBEDDING_TIMEOUT_MS', 60000);
95
+ return this.master ?? parseTimeoutMs('SPECMEM_FIND_EMBEDDING_TIMEOUT_MS', 120000);
96
96
  },
97
97
  /**
98
98
  * Timeout for health check embedding tests
@@ -140,10 +140,10 @@ export const embeddingTimeouts = {
140
140
  /**
141
141
  * Code search/pointer lookup timeout
142
142
  * Used by find_code_pointers tool
143
- * Env: SPECMEM_CODE_SEARCH_TIMEOUT (default: 60000 - increased from 30s)
143
+ * Env: SPECMEM_CODE_SEARCH_TIMEOUT (default: 120000 - increased from 60s to match cold-start initial timeout)
144
144
  */
145
145
  get codeSearch() {
146
- return this.master ?? parseTimeoutMs('SPECMEM_CODE_SEARCH_TIMEOUT', 60000);
146
+ return this.master ?? parseTimeoutMs('SPECMEM_CODE_SEARCH_TIMEOUT', 120000);
147
147
  },
148
148
  /**
149
149
  * Search timeout for database queries in find_memory
package/dist/database.js CHANGED
@@ -172,28 +172,70 @@ export class DatabaseManager {
172
172
  return undefined;
173
173
  }
174
174
  createPool() {
175
+ // Issue #11 FIX: All pool settings are env-var configurable with sensible defaults
176
+ const poolMax = parseInt(process.env['SPECMEM_DB_POOL_MAX'] || String(this.config.maxConnections || 20), 10);
177
+ const connectionTimeoutMillis = parseInt(process.env['SPECMEM_DB_CONNECTION_TIMEOUT_MS'] || String(this.config.connectionTimeout || 10000), 10);
178
+ const statementTimeoutMs = parseInt(process.env['SPECMEM_DB_STATEMENT_TIMEOUT_MS'] || '30000', 10);
179
+ const idleInTransactionTimeoutMs = parseInt(process.env['SPECMEM_DB_IDLE_TRANSACTION_TIMEOUT_MS'] || '60000', 10);
180
+ logger.info({
181
+ poolMax,
182
+ connectionTimeoutMillis,
183
+ statementTimeoutMs,
184
+ idleInTransactionTimeoutMs
185
+ }, 'Creating pool with configurable timeouts (Issue #11 fix)');
175
186
  return new Pool({
176
187
  host: this.config.host,
177
188
  port: this.config.port,
178
189
  database: this.config.database,
179
190
  user: this.config.user,
180
191
  password: this.config.password,
181
- max: this.config.maxConnections,
192
+ max: poolMax,
182
193
  idleTimeoutMillis: this.config.idleTimeout,
183
- connectionTimeoutMillis: this.config.connectionTimeout,
184
- ssl: this.config.ssl
194
+ connectionTimeoutMillis: connectionTimeoutMillis,
195
+ ssl: this.config.ssl,
196
+ // Issue #11: Set statement_timeout and idle_in_transaction_session_timeout
197
+ // These prevent hung queries from blocking the pool over long sessions
198
+ statement_timeout: statementTimeoutMs,
199
+ idle_in_transaction_session_timeout: idleInTransactionTimeoutMs
185
200
  });
186
201
  }
187
202
  setupPoolEvents() {
203
+ // Issue #11 FIX: Enhanced pool error handler with connection details
188
204
  this.pool.on('error', (err) => {
189
- logger.error({ err }, 'Unexpected pool error');
205
+ logger.error({
206
+ err,
207
+ totalCount: this.pool.totalCount,
208
+ idleCount: this.pool.idleCount,
209
+ waitingCount: this.pool.waitingCount
210
+ }, 'Unexpected pool error - logging pool health for diagnostics');
190
211
  });
191
212
  // SCHEMA ISOLATION FIX: pool.on('connect') doesn't wait for async callbacks!
192
213
  // The client is returned to the caller before await completes.
193
214
  // We still set search_path here for defense-in-depth, but critical paths
194
215
  // must use ensureSearchPath() explicitly to guarantee isolation.
195
216
  this.pool.on('connect', (client) => {
196
- logger.debug('New client connected to pool');
217
+ // Issue #11 FIX: Set statement_timeout and idle_in_transaction_session_timeout
218
+ // on each new connection as defense-in-depth (in addition to pool-level config).
219
+ // This ensures timeouts apply even if the pool config is ignored by the driver.
220
+ const statementTimeoutMs = parseInt(process.env['SPECMEM_DB_STATEMENT_TIMEOUT_MS'] || '30000', 10);
221
+ const idleInTransactionTimeoutMs = parseInt(process.env['SPECMEM_DB_IDLE_TRANSACTION_TIMEOUT_MS'] || '60000', 10);
222
+ logger.debug({
223
+ totalCount: this.pool.totalCount,
224
+ idleCount: this.pool.idleCount,
225
+ waitingCount: this.pool.waitingCount
226
+ }, 'New client connected to pool');
227
+ // Set statement_timeout and idle_in_transaction_session_timeout via SET command
228
+ // This is fire-and-forget (pg doesn't await connect handlers)
229
+ client.query('SET statement_timeout TO ' + statementTimeoutMs)
230
+ .then(() => {
231
+ return client.query('SET idle_in_transaction_session_timeout TO ' + idleInTransactionTimeoutMs);
232
+ })
233
+ .then(() => {
234
+ logger.debug({ statementTimeoutMs, idleInTransactionTimeoutMs }, 'Set query timeouts on new pool connection');
235
+ })
236
+ .catch((error) => {
237
+ logger.error({ error }, 'Failed to set query timeouts on new connection');
238
+ });
197
239
  // If we have a current schema, set search_path on this new connection
198
240
  // NOTE: This is fire-and-forget because pg doesn't await connect handlers
199
241
  // Critical code paths should call ensureSearchPath() explicitly
@@ -211,7 +253,11 @@ export class DatabaseManager {
211
253
  }
212
254
  });
213
255
  this.pool.on('remove', () => {
214
- logger.debug('Client removed from pool');
256
+ logger.debug({
257
+ totalCount: this.pool.totalCount,
258
+ idleCount: this.pool.idleCount,
259
+ waitingCount: this.pool.waitingCount
260
+ }, 'Client removed from pool');
215
261
  });
216
262
  }
217
263
  async initialize() {
@@ -636,8 +636,8 @@ export class BigBrainMigrations {
636
636
  )
637
637
  );
638
638
 
639
- -- index for content hash deduplication
640
- CREATE UNIQUE INDEX IF NOT EXISTS idx_codebase_files_content_hash
639
+ -- index for content hash lookup (NOT unique - multiple files can share same hash)
640
+ CREATE INDEX IF NOT EXISTS idx_codebase_files_content_hash
641
641
  ON codebase_files(content_hash);
642
642
 
643
643
  -- full-text search index for code search
@@ -4203,10 +4203,11 @@ export class BigBrainMigrations {
4203
4203
  CREATE INDEX IF NOT EXISTS idx_memories_project_path_type
4204
4204
  ON memories(project_path, memory_type);
4205
4205
 
4206
- -- Composite index for codebase_files project + file_path
4207
- -- Used by: codebaseTools queries filtering by project_path and file_path
4208
- CREATE INDEX IF NOT EXISTS idx_codebase_files_project_path_file
4209
- ON codebase_files(project_path, file_path);
4206
+ -- UNIQUE composite index for codebase_files project + file_path
4207
+ -- MUST be UNIQUE for ON CONFLICT (file_path, project_path) upsert to work
4208
+ -- Used by: changeHandler.updateCodebaseFiles, codebaseTools queries
4209
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_codebase_files_path_project_unique
4210
+ ON codebase_files(file_path, project_path);
4210
4211
 
4211
4212
  -- Composite index for codebase_files project + content_hash (dedup check)
4212
4213
  -- Used by: codebaseIndexer for hash lookups per project
@@ -52,7 +52,7 @@ CREATE TABLE IF NOT EXISTS team_member_conversations (
52
52
  );
53
53
 
54
54
  CREATE INDEX IF NOT EXISTS idx_team_member_conversations_memory ON team_member_conversations(memory_id);
55
- CREATE INDEX IF NOT EXISTS idx_team_member_conversations_team member ON team_member_conversations(team_member_id);
55
+ CREATE INDEX IF NOT EXISTS idx_team_member_conversations_team_member ON team_member_conversations(team_member_id);
56
56
  CREATE INDEX IF NOT EXISTS idx_team_member_conversations_time ON team_member_conversations(timestamp);
57
57
 
58
58
  -- HELPER FUNCTIONS
@@ -266,6 +266,27 @@ INSERT INTO team_channels (name, channel_type, created_by, project_path)
266
266
  SELECT 'team-broadcast', 'broadcast', 'system', '/'
267
267
  WHERE NOT EXISTS (SELECT 1 FROM team_channels WHERE name = 'team-broadcast' AND channel_type = 'broadcast');
268
268
 
269
+ -- ============================================================================
270
+ -- TEAM MEMBER CONVERSATIONS TABLE
271
+ -- Stores the conversation context that spawned a memory
272
+ -- Required by MemoryDrilldown.js for getMemoryFull / drill_down
273
+ -- ============================================================================
274
+ CREATE TABLE IF NOT EXISTS team_member_conversations (
275
+ id BIGSERIAL PRIMARY KEY,
276
+ memory_id UUID NOT NULL,
277
+ team_member_id VARCHAR(255) NOT NULL,
278
+ team_member_name VARCHAR(255),
279
+ timestamp TIMESTAMPTZ DEFAULT NOW(),
280
+ summary TEXT,
281
+ full_transcript TEXT,
282
+ message_count INTEGER,
283
+ created_at TIMESTAMPTZ DEFAULT NOW()
284
+ );
285
+
286
+ CREATE INDEX IF NOT EXISTS idx_team_member_conversations_memory ON team_member_conversations(memory_id);
287
+ CREATE INDEX IF NOT EXISTS idx_team_member_conversations_team_member ON team_member_conversations(team_member_id);
288
+ CREATE INDEX IF NOT EXISTS idx_team_member_conversations_time ON team_member_conversations(timestamp);
289
+
269
290
  -- ============================================================================
270
291
  -- END - Core data uses PUBLIC schema with project_path filtering
271
292
  -- ============================================================================