2ndbrain 2026.2.2 → 2026.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "2ndbrain",
3
- "version": "2026.2.2",
3
+ "version": "2026.2.3",
4
4
  "description": "Always-on Node.js service bridging Telegram messaging to Claude AI with knowledge graph, journal, project management, and semantic search.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { EventEmitter } from 'node:events';
3
+ import fs from 'node:fs';
3
4
  import path from 'node:path';
4
5
 
5
6
  /**
@@ -42,6 +43,11 @@ class ClaudeBridge extends EventEmitter {
42
43
  const startTime = Date.now();
43
44
  const args = this._buildArgs(sessionId, systemPrompt);
44
45
 
46
+ // Pre-spawn MCP config validation (new sessions only)
47
+ if (!sessionId) {
48
+ this._validateMcpConfig();
49
+ }
50
+
45
51
  this.logger.info('claude', `Spawning: claude ${args.join(' ')}`);
46
52
 
47
53
  const runtimeDir = path.join(this.config.DATA_DIR, 'claude-runtime');
@@ -54,6 +60,7 @@ class ClaudeBridge extends EventEmitter {
54
60
  });
55
61
 
56
62
  this.activeProcess = proc;
63
+ this.logger.info('claude', `Subprocess spawned (pid=${proc.pid}, cwd=${runtimeDir})`);
57
64
 
58
65
  let stdoutBuffer = '';
59
66
  let stderrBuffer = '';
@@ -63,17 +70,38 @@ class ClaudeBridge extends EventEmitter {
63
70
  let timedOut = false;
64
71
  let receivedFirstOutput = false;
65
72
 
66
- // Startup watchdog: warn if no stdout arrives within 30s
67
- const startupTimeout = setTimeout(() => {
68
- if (!receivedFirstOutput) {
69
- this.logger.warn(
73
+ // Progressive startup watchdog: warn at escalating intervals if no stdout arrives.
74
+ // Delays between checks: 10s, 10s, 10s, 15s, 15s → cumulative: 10, 20, 30, 45, 60s, then every 30s.
75
+ const WATCHDOG_DELAYS = [10_000, 10_000, 10_000, 15_000, 15_000];
76
+ let watchdogStep = 0;
77
+ let startupTimeout = null;
78
+
79
+ const scheduleWatchdog = () => {
80
+ const delay = watchdogStep < WATCHDOG_DELAYS.length
81
+ ? WATCHDOG_DELAYS[watchdogStep]
82
+ : 30_000;
83
+
84
+ startupTimeout = setTimeout(() => {
85
+ if (receivedFirstOutput) return;
86
+
87
+ const elapsed = Date.now() - startTime;
88
+ const level = elapsed >= 30_000 ? 'warn' : 'info';
89
+ const stderrTail = stderrBuffer.trim()
90
+ ? stderrBuffer.trim().slice(-500)
91
+ : '(empty)';
92
+
93
+ this.logger[level](
70
94
  'claude',
71
- 'No output received from Claude CLI within 30s of spawn -- ' +
72
- 'subprocess may be stuck during MCP server initialization or permission prompt. ' +
73
- `stderr so far: ${stderrBuffer.trim() || '(empty)'}`,
95
+ `No stdout after ${Math.round(elapsed / 1000)}s (pid=${proc.pid}) -- ` +
96
+ 'possible causes: MCP server init (npx download), API queueing, network issue. ' +
97
+ `stderr tail: ${stderrTail}`,
74
98
  );
75
- }
76
- }, 30_000);
99
+
100
+ watchdogStep++;
101
+ scheduleWatchdog();
102
+ }, delay);
103
+ };
104
+ scheduleWatchdog();
77
105
 
78
106
  // Set up the timeout guard
79
107
  const timeout = setTimeout(() => {
@@ -85,13 +113,14 @@ class ClaudeBridge extends EventEmitter {
85
113
  // Pipe the user message via stdin and close it
86
114
  proc.stdin.write(message);
87
115
  proc.stdin.end();
116
+ this.logger.info('claude', `Message piped to stdin and closed (${message.length} chars)`);
88
117
 
89
118
  // Collect and parse stdout stream-json chunks
90
119
  proc.stdout.on('data', (chunk) => {
91
120
  if (!receivedFirstOutput) {
92
121
  receivedFirstOutput = true;
93
122
  clearTimeout(startupTimeout);
94
- this.logger.debug('claude', `First output received after ${Date.now() - startTime}ms`);
123
+ this.logger.info('claude', `First stdout received after ${Date.now() - startTime}ms (pid=${proc.pid})`);
95
124
  }
96
125
 
97
126
  stdoutBuffer += chunk.toString();
@@ -119,19 +148,27 @@ class ClaudeBridge extends EventEmitter {
119
148
  }
120
149
  });
121
150
 
122
- // Monitor stderr for errors (log in real time for diagnostics)
151
+ // Monitor stderr for errors (log in real time for diagnostics).
152
+ // During startup (before first stdout), promote to INFO so MCP init
153
+ // messages (npx downloads, connection errors) are visible at default log level.
123
154
  proc.stderr.on('data', (chunk) => {
124
155
  const text = chunk.toString();
125
156
  stderrBuffer += text;
126
157
  for (const line of text.split('\n')) {
127
158
  const trimmed = line.trim();
128
159
  if (trimmed) {
129
- this.logger.debug('claude-stderr', trimmed);
160
+ if (!receivedFirstOutput) {
161
+ this.logger.info('claude-stderr', trimmed);
162
+ } else {
163
+ this.logger.debug('claude-stderr', trimmed);
164
+ }
130
165
  }
131
166
  }
132
167
  });
133
168
 
134
169
  proc.on('close', (code) => {
170
+ const elapsed = Date.now() - startTime;
171
+ this.logger.info('claude', `Subprocess exited (pid=${proc.pid}, code=${code}, elapsed=${elapsed}ms)`);
135
172
  clearTimeout(timeout);
136
173
  clearTimeout(startupTimeout);
137
174
  this.activeProcess = null;
@@ -234,6 +271,37 @@ class ClaudeBridge extends EventEmitter {
234
271
  return args;
235
272
  }
236
273
 
274
+ /**
275
+ * Validate MCP config file before spawning. Logs diagnostics for common
276
+ * issues (missing file, npx-based servers that may download on first run).
277
+ * Does NOT block the spawn -- purely diagnostic.
278
+ * @private
279
+ */
280
+ _validateMcpConfig() {
281
+ const configPath = this.config.MCP_CONFIG_PATH;
282
+ if (!configPath) {
283
+ this.logger.info('claude', 'No MCP_CONFIG_PATH configured; skipping MCP config validation');
284
+ return;
285
+ }
286
+
287
+ try {
288
+ const raw = fs.readFileSync(configPath, 'utf-8');
289
+ const parsed = JSON.parse(raw);
290
+ const servers = parsed.mcpServers || {};
291
+ const serverNames = Object.keys(servers);
292
+
293
+ this.logger.info('claude', `MCP config: ${configPath} (${serverNames.length} server(s): ${serverNames.join(', ')})`);
294
+
295
+ for (const [name, server] of Object.entries(servers)) {
296
+ if (server.command === 'npx') {
297
+ this.logger.info('claude', `MCP server "${name}" uses npx -- first-run download may cause startup delay`);
298
+ }
299
+ }
300
+ } catch (err) {
301
+ this.logger.warn('claude', `MCP config validation failed (${configPath}): ${err.message}`);
302
+ }
303
+ }
304
+
237
305
  /**
238
306
  * Handle a single parsed stream-json chunk.
239
307
  *
package/src/index.js CHANGED
@@ -409,7 +409,9 @@ async function main() {
409
409
  mcpConfigPath = generateMcpConfig(config);
410
410
  // Override MCP_CONFIG_PATH to use the generated one
411
411
  config.MCP_CONFIG_PATH = mcpConfigPath;
412
- logger.info('startup', `MCP config written to ${mcpConfigPath}`);
412
+ const mcpContent = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf-8'));
413
+ const mcpServerNames = Object.keys(mcpContent.mcpServers || {});
414
+ logger.info('startup', `MCP config written to ${mcpConfigPath} (servers: ${mcpServerNames.join(', ')})`);
413
415
  } catch (err) {
414
416
  logger.error('startup', `Failed to generate MCP config: ${err.message}`);
415
417
  }