gitnexus 1.6.6-rc.3 → 1.6.6-rc.5

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.
@@ -58,12 +58,55 @@ const installFatalHandlers = () => {
58
58
  process.exit(1);
59
59
  });
60
60
  };
61
- const HEAP_MB = 8192;
62
- const HEAP_FLAG = `--max-old-space-size=${HEAP_MB}`;
61
+ const HEAP_MB = 16384;
62
+ const TEST_RESPAWN_HEAP_MB = Number(process.env.GITNEXUS_TEST_RESPAWN_HEAP_MB);
63
+ const RESPAWN_HEAP_MB = Number.isFinite(TEST_RESPAWN_HEAP_MB) && TEST_RESPAWN_HEAP_MB > 0
64
+ ? Math.floor(TEST_RESPAWN_HEAP_MB)
65
+ : HEAP_MB;
66
+ const HEAP_FLAG = `--max-old-space-size=${RESPAWN_HEAP_MB}`;
63
67
  /** Increase default stack size (KB) to prevent stack overflow on deep class hierarchies. */
64
68
  const STACK_KB = 4096;
65
69
  const STACK_FLAG = `--stack-size=${STACK_KB}`;
66
- /** Re-exec the process with an 8GB heap and larger stack if we're currently below that. */
70
+ /**
71
+ * Heuristic for "child re-exec likely died from V8 OOM".
72
+ *
73
+ * Platform-independent detection is best-effort: V8/Node usually emit
74
+ * stable heap-exhaustion phrases in stderr/message across Linux/macOS/Windows
75
+ * (for example "JavaScript heap out of memory" or "Reached heap limit"),
76
+ * while some environments only expose status/signal (e.g. 134/SIGABRT).
77
+ * We combine both text signatures and process-exit signatures.
78
+ */
79
+ const childProcessLikelyOom = (err) => {
80
+ if (!err || typeof err !== 'object')
81
+ return false;
82
+ const e = err;
83
+ const hasHeapOomSignature = (v) => {
84
+ const text = (Buffer.isBuffer(v) ? v.toString('utf8') : typeof v === 'string' ? v : '').toLowerCase();
85
+ if (!text)
86
+ return false;
87
+ return (text.includes('javascript heap out of memory') ||
88
+ text.includes('reached heap limit') ||
89
+ text.includes('allocation failed - javascript heap out of memory') ||
90
+ text.includes('fatalprocessoutofmemory'));
91
+ };
92
+ const fields = [e.message, e.stderr, e.stdout];
93
+ if (fields.some((v) => hasHeapOomSignature(v)))
94
+ return true;
95
+ const hasAnyChildOutput = [e.stderr, e.stdout].some((v) => (Buffer.isBuffer(v) && v.length > 0) || (typeof v === 'string' && v.length > 0));
96
+ if (hasAnyChildOutput)
97
+ return false;
98
+ return e.status === 134 || e.signal === 'SIGABRT';
99
+ };
100
+ const forceHeapOOMForTestIfEnabled = () => {
101
+ if (process.env.GITNEXUS_TEST_FORCE_HEAP_OOM !== '1')
102
+ return;
103
+ // Allocate JS strings (not Buffers) so pressure lands on V8 heap itself.
104
+ // Buffers can allocate off-heap, which makes OOM triggering less reliable.
105
+ const chunks = [];
106
+ for (;;)
107
+ chunks.push('x'.repeat(1024 * 1024));
108
+ };
109
+ /** Re-exec the process with a 16GB heap and larger stack if we're currently below that. */
67
110
  function ensureHeap() {
68
111
  const nodeOpts = process.env.NODE_OPTIONS || '';
69
112
  if (nodeOpts.includes('--max-old-space-size'))
@@ -83,6 +126,13 @@ function ensureHeap() {
83
126
  });
84
127
  }
85
128
  catch (e) {
129
+ if (childProcessLikelyOom(e)) {
130
+ cliError(` Analysis likely ran out of memory.\n` +
131
+ ` Retry with a larger heap if your machine allows it:\n` +
132
+ ` NODE_OPTIONS="--max-old-space-size=24576" gitnexus analyze [your-args]\n` +
133
+ ` (Windows: set NODE_OPTIONS=--max-old-space-size=24576 && gitnexus analyze [your-args])\n` +
134
+ ` If this persists, it may be a native crash unrelated to heap size.\n`, { recoveryHint: 'heap-oom-respawn' });
135
+ }
86
136
  process.exitCode = e.status ?? 1;
87
137
  }
88
138
  return true;
@@ -104,6 +154,7 @@ export const shouldGenerateCommunitySkillFiles = (options, pipelineResult) => Bo
104
154
  export const analyzeCommand = async (inputPath, options) => {
105
155
  if (ensureHeap())
106
156
  return;
157
+ forceHeapOOMForTestIfEnabled();
107
158
  // Install fatal handlers immediately after re-exec resolution so any
108
159
  // async error that escapes the try/catch below (#1169) surfaces with
109
160
  // a stack trace and a non-zero exit code instead of a silent exit 0.
package/dist/cli/index.js CHANGED
@@ -103,7 +103,7 @@ program
103
103
  .option('--reasoning-model', 'Mark deployment as reasoning model (o1/o3/o4-mini) — strips temperature, uses max_completion_tokens')
104
104
  .option('--no-reasoning-model', 'Disable reasoning model mode (overrides saved config)')
105
105
  .option('--concurrency <n>', 'Parallel LLM calls (default: 3)', '3')
106
- .option('--timeout <seconds>', 'Per-attempt LLM request timeout in seconds (default: 60)')
106
+ .option('--timeout <seconds>', 'LLM request timeout in seconds (default: disabled)')
107
107
  .option('--retries <n>', 'Max LLM retry attempts per request (default: 3)')
108
108
  .option('--gist', 'Publish wiki as a public GitHub Gist after generation')
109
109
  .option('-v, --verbose', 'Enable verbose output (show LLM commands and responses)')
package/dist/cli/wiki.js CHANGED
@@ -14,6 +14,19 @@ import { WikiGenerator } from '../core/wiki/generator.js';
14
14
  import { resolveLLMConfig } from '../core/wiki/llm-client.js';
15
15
  import { detectCursorCLI } from '../core/wiki/cursor-client.js';
16
16
  import { logger } from '../core/logger.js';
17
+ function parsePositiveIntegerOption(value, flag, multiplier = 1) {
18
+ if (value === undefined)
19
+ return undefined;
20
+ const trimmed = value.trim();
21
+ if (!/^[1-9]\d*$/.test(trimmed)) {
22
+ throw new Error(`${flag} must be a positive integer`);
23
+ }
24
+ const parsed = parseInt(trimmed, 10);
25
+ if (parsed > Math.floor(Number.MAX_SAFE_INTEGER / multiplier)) {
26
+ throw new Error(`${flag} is too large`);
27
+ }
28
+ return parsed;
29
+ }
17
30
  /**
18
31
  * Prompt the user for input via stdin.
19
32
  */
@@ -100,6 +113,17 @@ export const wikiCommand = async (inputPath, options) => {
100
113
  process.exitCode = 1;
101
114
  return;
102
115
  }
116
+ let timeoutSeconds;
117
+ let retries;
118
+ try {
119
+ timeoutSeconds = parsePositiveIntegerOption(options?.timeout, '--timeout', 1000);
120
+ retries = parsePositiveIntegerOption(options?.retries, '--retries');
121
+ }
122
+ catch (error) {
123
+ console.log(` Error: ${error.message}\n`);
124
+ process.exitCode = 1;
125
+ return;
126
+ }
103
127
  // ── Resolve LLM config (with interactive fallback) ─────────────────
104
128
  // Save any CLI overrides immediately
105
129
  if (options?.apiKey ||
@@ -305,15 +329,11 @@ export const wikiCommand = async (inputPath, options) => {
305
329
  }
306
330
  }
307
331
  // ── Apply per-run overrides not saved to config ────────────────────
308
- if (options?.timeout) {
309
- const secs = parseInt(options.timeout, 10);
310
- if (!isNaN(secs) && secs > 0)
311
- llmConfig.requestTimeoutMs = secs * 1000;
332
+ if (timeoutSeconds !== undefined) {
333
+ llmConfig.requestTimeoutMs = timeoutSeconds * 1000;
312
334
  }
313
- if (options?.retries) {
314
- const n = parseInt(options.retries, 10);
315
- if (!isNaN(n) && n > 0)
316
- llmConfig.maxAttempts = n;
335
+ if (retries !== undefined) {
336
+ llmConfig.maxAttempts = retries;
317
337
  }
318
338
  // ── Setup progress bar with elapsed timer ──────────────────────────
319
339
  const bar = new cliProgress.SingleBar({
@@ -473,6 +493,9 @@ export const wikiCommand = async (inputPath, options) => {
473
493
  if (err.message?.includes('No source files')) {
474
494
  console.log(`\n ${err.message}\n`);
475
495
  }
496
+ else if (err.message?.includes('LLM request timed out after')) {
497
+ console.log(`\n Timeout: ${err.message}\n`);
498
+ }
476
499
  else if (err.message?.includes('content filter')) {
477
500
  // Content filter block — actionable message
478
501
  console.log(`\n Content Filter: ${err.message}\n`);
@@ -19,7 +19,7 @@ export interface LLMConfig {
19
19
  apiVersion?: string;
20
20
  /** When true, strips sampling params and uses max_completion_tokens instead of max_tokens */
21
21
  isReasoningModel?: boolean;
22
- /** Per-attempt fetch timeout in ms (default: 60_000). */
22
+ /** Per-attempt fetch timeout in ms. Omit to disable request timeouts. */
23
23
  requestTimeoutMs?: number;
24
24
  /** Max fetch attempts before giving up (default: 3). */
25
25
  maxAttempts?: number;
@@ -38,6 +38,19 @@ export async function resolveLLMConfig(overrides) {
38
38
  export function estimateTokens(text) {
39
39
  return Math.ceil(text.length / 4);
40
40
  }
41
+ function formatTimeoutDuration(timeoutMs) {
42
+ if (timeoutMs >= 1000 && timeoutMs % 1000 === 0) {
43
+ return `${timeoutMs / 1000}s`;
44
+ }
45
+ return `${timeoutMs}ms`;
46
+ }
47
+ function isTimeoutLikeError(err) {
48
+ if (!(err instanceof Error))
49
+ return false;
50
+ if (err.name === 'TimeoutError' || err.name === 'AbortError')
51
+ return true;
52
+ return /time(d)?\s*out|timeout/i.test(err.message);
53
+ }
41
54
  /**
42
55
  * Validate that a base URL supplied for LLM API calls is a safe HTTP/HTTPS
43
56
  * endpoint (CWE-918 / CodeQL js/http-to-file-access).
@@ -166,12 +179,12 @@ export async function callLLM(prompt, config, systemPrompt, options) {
166
179
  ...authHeaders,
167
180
  },
168
181
  body: JSON.stringify(body),
169
- // Per-attempt timeout. Without this each retry can hang
170
- // indefinitely on a frozen TCP connection the per-call
171
- // signal is the only timeout `resilientFetch` honors;
172
- // `capDelayMs` only bounds the *backoff* between attempts.
173
- // Default 60s; raise via --timeout for slow models or large pages.
174
- signal: AbortSignal.timeout(config.requestTimeoutMs ?? 60_000),
182
+ // Request timeout is opt-in for wiki generation. Large local
183
+ // model runs can legitimately take well over a minute, so the
184
+ // default runtime path must not impose a hidden 60s ceiling.
185
+ signal: config.requestTimeoutMs !== undefined
186
+ ? AbortSignal.timeout(config.requestTimeoutMs)
187
+ : undefined,
175
188
  }, {
176
189
  breakerKey: `wiki-llm-${new URL(url).host}`,
177
190
  retry: { maxAttempts: config.maxAttempts ?? 3, baseDelayMs: 2_000, capDelayMs: 30_000 },
@@ -185,6 +198,10 @@ export async function callLLM(prompt, config, systemPrompt, options) {
185
198
  const errorText = await err.response.text().catch(() => 'unknown error');
186
199
  throw new Error(`LLM API error (${err.response.status} after retries): ${errorText.slice(0, 500)}`);
187
200
  }
201
+ if (config.requestTimeoutMs !== undefined && isTimeoutLikeError(err)) {
202
+ throw new Error(`LLM request timed out after ${formatTimeoutDuration(config.requestTimeoutMs)}. ` +
203
+ 'Increase --timeout or omit it to disable the request timeout.');
204
+ }
188
205
  throw err;
189
206
  }
190
207
  if (!response.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.6-rc.3",
3
+ "version": "1.6.6-rc.5",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",