viberag 0.7.0 → 0.8.0

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.
@@ -197,7 +197,7 @@ Manual MCP Setup:
197
197
  const settings = await loadUserSettings();
198
198
  const effective = resolveEffectiveTelemetryMode(settings);
199
199
  if (effective.mode === 'disabled') {
200
- addOutput('system', `Telemetry is disabled, so error reporting is also disabled.\n\nSet with:\n /telemetry default\n\nThen re-run:\n /test-exception`);
200
+ addOutput('system', `Telemetry is disabled, so error reporting is also disabled.\n\nSet with:\n /telemetry stripped\n\nThen re-run:\n /test-exception`);
201
201
  return;
202
202
  }
203
203
  addOutput('system', `Triggering test exceptions (test_id=${testId}).\nThis is an undocumented command.`);
@@ -77,7 +77,7 @@ export declare class DaemonClient {
77
77
  /**
78
78
  * Index the codebase.
79
79
  */
80
- index(options?: ClientIndexOptions): Promise<IndexStats>;
80
+ index(options?: ClientIndexOptions, timeoutMs?: number): Promise<IndexStats>;
81
81
  /**
82
82
  * Start indexing asynchronously.
83
83
  */
@@ -152,12 +152,12 @@ export class DaemonClient {
152
152
  await this.connect();
153
153
  }
154
154
  }
155
- async request(method, params) {
155
+ async request(method, params, timeoutMs) {
156
156
  await this.ensureConnected();
157
157
  const withMeta = params
158
158
  ? { ...params, __client: { source: this.clientSource } }
159
159
  : { __client: { source: this.clientSource } };
160
- return this.connection.request(method, withMeta);
160
+ return this.connection.request(method, withMeta, timeoutMs);
161
161
  }
162
162
  /**
163
163
  * Search the codebase.
@@ -195,8 +195,8 @@ export class DaemonClient {
195
195
  /**
196
196
  * Index the codebase.
197
197
  */
198
- async index(options) {
199
- return this.request('index', options);
198
+ async index(options, timeoutMs) {
199
+ return this.request('index', options, timeoutMs);
200
200
  }
201
201
  /**
202
202
  * Start indexing asynchronously.
@@ -96,6 +96,12 @@ export interface DaemonStatusResponse {
96
96
  totalRefs?: number;
97
97
  embeddingProvider?: string;
98
98
  embeddingModel?: string;
99
+ memory: {
100
+ rssMB: number;
101
+ heapUsedMB: number;
102
+ externalMB: number;
103
+ arrayBuffersMB: number;
104
+ };
99
105
  warmupStatus: string;
100
106
  warmupElapsedMs?: number;
101
107
  warmupCancelRequestedAt?: string | null;
@@ -257,23 +257,29 @@ export class Chunker {
257
257
  // Set parser language
258
258
  const language = this.languages.get(lang);
259
259
  this.parser.setLanguage(language);
260
- // Parse the content
260
+ // Parse the content. Tree-sitter trees own native/WASM allocations and
261
+ // must be explicitly deleted once extraction is complete.
261
262
  const tree = this.parser.parse(content);
262
263
  // If parsing failed, fall back to module chunk (with size enforcement + overlap)
263
264
  if (!tree) {
264
265
  const moduleChunk = this.createModuleChunk(filepath, content);
265
266
  return this.enforceSizeLimits([moduleChunk], maxChunkSize, content, lang, filepath, DEFAULT_OVERLAP_LINES);
266
267
  }
267
- // Extract chunks based on language with context tracking
268
- const chunks = this.extractChunks(tree.rootNode, content, lang, filepath, maxChunkSize);
269
- // If no chunks found, fall back to module chunk (with size enforcement + overlap)
270
- if (chunks.length === 0) {
271
- const moduleChunk = this.createModuleChunk(filepath, content);
272
- return this.enforceSizeLimits([moduleChunk], maxChunkSize, content, lang, filepath, DEFAULT_OVERLAP_LINES);
268
+ try {
269
+ // Extract chunks based on language with context tracking
270
+ const chunks = this.extractChunks(tree.rootNode, content, lang, filepath, maxChunkSize);
271
+ // If no chunks found, fall back to module chunk (with size enforcement + overlap)
272
+ if (chunks.length === 0) {
273
+ const moduleChunk = this.createModuleChunk(filepath, content);
274
+ return this.enforceSizeLimits([moduleChunk], maxChunkSize, content, lang, filepath, DEFAULT_OVERLAP_LINES);
275
+ }
276
+ // Split oversized chunks and merge tiny ones
277
+ const sizedChunks = this.enforceSizeLimits(chunks, maxChunkSize, content, lang, filepath);
278
+ return sizedChunks;
279
+ }
280
+ finally {
281
+ tree.delete();
273
282
  }
274
- // Split oversized chunks and merge tiny ones
275
- const sizedChunks = this.enforceSizeLimits(chunks, maxChunkSize, content, lang, filepath);
276
- return sizedChunks;
277
283
  }
278
284
  /**
279
285
  * Analyze a file by parsing once and extracting:
@@ -314,6 +320,7 @@ export class Chunker {
314
320
  }
315
321
  const language = this.languages.get(lang);
316
322
  this.parser.setLanguage(language);
323
+ // Parse once and guarantee tree cleanup via finally.
317
324
  const tree = this.parser.parse(content);
318
325
  if (!tree) {
319
326
  const moduleChunk = this.createModuleChunk(filepath, content);
@@ -325,16 +332,21 @@ export class Chunker {
325
332
  refs: [],
326
333
  };
327
334
  }
328
- const definition_chunks = this.extractChunks(tree.rootNode, content, lang, filepath, definitionMaxChunkSize);
329
- const chunks = this.enforceSizeLimits(this.extractChunks(tree.rootNode, content, lang, filepath, chunkMaxSize), chunkMaxSize, content, lang, filepath);
330
- const refs = this.extractRefsFromTree(tree.rootNode, lang, refsOptions);
331
- return {
332
- language: lang,
333
- parse_status: 'parsed',
334
- definition_chunks,
335
- chunks,
336
- refs,
337
- };
335
+ try {
336
+ const definition_chunks = this.extractChunks(tree.rootNode, content, lang, filepath, definitionMaxChunkSize);
337
+ const chunks = this.enforceSizeLimits(this.extractChunks(tree.rootNode, content, lang, filepath, chunkMaxSize), chunkMaxSize, content, lang, filepath);
338
+ const refs = this.extractRefsFromTree(tree.rootNode, lang, refsOptions);
339
+ return {
340
+ language: lang,
341
+ parse_status: 'parsed',
342
+ definition_chunks,
343
+ chunks,
344
+ refs,
345
+ };
346
+ }
347
+ finally {
348
+ tree.delete();
349
+ }
338
350
  }
339
351
  /**
340
352
  * Extract chunks from a syntax tree.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * PostHog telemetry client wrapper for VibeRAG.
3
3
  *
4
- * - Telemetry is enabled by default (opt-out).
4
+ * - Telemetry is enabled by default in stripped mode (opt-out).
5
5
  * - Settings are global under VIBERAG_HOME and shared by CLI/daemon/MCP.
6
6
  * - Captures inputs/outputs but strips file contents / code text.
7
7
  */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * PostHog telemetry client wrapper for VibeRAG.
3
3
  *
4
- * - Telemetry is enabled by default (opt-out).
4
+ * - Telemetry is enabled by default in stripped mode (opt-out).
5
5
  * - Settings are global under VIBERAG_HOME and shared by CLI/daemon/MCP.
6
6
  * - Captures inputs/outputs but strips file contents / code text.
7
7
  */
@@ -1 +1 @@
1
- export declare const VIBERAG_PRIVACY_POLICY = "VibeRAG Privacy Policy (Telemetry + Error Reporting)\n\nEffective date: 2026-01-31\n\nVibeRAG is a local developer tool. Some features send telemetry and error reports to help improve performance, reliability, and usability.\n\nThis policy describes what VibeRAG collects and how to opt out. This is not legal advice.\n\n1) Where data is sent\n\n- Telemetry and survey events are sent to PostHog.\n- Error reports (exceptions and stack traces) are sent to Sentry.\n\n2) Telemetry modes\n\nVibeRAG supports three telemetry modes:\n\n- disabled: no telemetry events or error reports are sent.\n- stripped: privacy-preserving telemetry (no query text; minimal metadata).\n- default: includes query text (with best-effort redaction) and richer structured metadata.\n\nTelemetry is enabled by default. You can change this at any time:\n\n- Run /telemetry disabled|stripped|default in the VibeRAG CLI.\n- Or set VIBERAG_TELEMETRY=disabled|stripped|default as an environment variable.\n\n3) What telemetry data we collect\n\nDepending on telemetry mode, VibeRAG may collect:\n\n- Tool/method names (e.g. codebase_search, get_symbol_details)\n- Timing and performance metrics (durations, counts, success/failure)\n- Inputs and outputs for operations, with important limitations below\n- Software/runtime info (VibeRAG version, Node version, OS platform, architecture)\n- A random installation identifier (UUID) to understand usage over time\n- A per-project identifier derived from a one-way hash of the project path\n\nFile contents / code text\n\nVibeRAG performs code search and navigation. Some tool outputs naturally contain code snippets or file lines.\n\nVibeRAG does not intentionally collect file contents or raw code text in telemetry. Before sending telemetry, VibeRAG strips fields that contain file contents/code text and replaces them with summaries (hashes, byte counts, line counts) plus structural metadata (IDs, file paths, line ranges, scores).\n\nQuery text\n\nIn default mode, VibeRAG may collect search query text. VibeRAG applies best-effort redaction of common secret patterns and truncates long strings, but cannot guarantee that all sensitive data is removed. If you work with sensitive data, use stripped or disabled.\n\n4) What error reporting data we collect (Sentry)\n\nWhen enabled, error reports may include:\n\n- exception type/message\n- stack traces\n- basic runtime metadata (OS, Node version, VibeRAG version, service name)\n\nVibeRAG does not intentionally include file contents or code text in error reports.\n\n5) How to opt out\n\n- Disable telemetry and error reporting: /telemetry disabled or VIBERAG_TELEMETRY=disabled\n- You can reset your installation identifier by deleting VibeRAG\u2019s global settings file under VIBERAG_HOME (default: ~/.local/share/viberag/settings.json).\n\n6) Contact\n\nFor questions about telemetry and privacy, open an issue in the VibeRAG repository.\n";
1
+ export declare const VIBERAG_PRIVACY_POLICY = "VibeRAG Privacy Policy (Telemetry + Error Reporting)\n\nEffective date: 2026-01-31\n\nVibeRAG is a local developer tool. Some features send telemetry and error reports to help improve performance, reliability, and usability.\n\nThis policy describes what VibeRAG collects and how to opt out. This is not legal advice.\n\n1) Where data is sent\n\n- Telemetry and survey events are sent to PostHog.\n- Error reports (exceptions and stack traces) are sent to Sentry.\n\n2) Telemetry modes\n\nVibeRAG supports three telemetry modes:\n\n- disabled: no telemetry events or error reports are sent.\n- stripped: privacy-preserving telemetry (no query text; minimal metadata).\n- default: includes query text (with best-effort redaction) and richer structured metadata.\n\nTelemetry defaults to stripped mode. You can change this at any time:\n\n- Run /telemetry disabled|stripped|default in the VibeRAG CLI.\n- Or set VIBERAG_TELEMETRY=disabled|stripped|default as an environment variable.\n\n3) What telemetry data we collect\n\nDepending on telemetry mode, VibeRAG may collect:\n\n- Tool/method names (e.g. codebase_search, get_symbol_details)\n- Timing and performance metrics (durations, counts, success/failure)\n- Inputs and outputs for operations, with important limitations below\n- Software/runtime info (VibeRAG version, Node version, OS platform, architecture)\n- A random installation identifier (UUID) to understand usage over time\n- A per-project identifier derived from a one-way hash of the project path\n\nFile contents / code text\n\nVibeRAG performs code search and navigation. Some tool outputs naturally contain code snippets or file lines.\n\nVibeRAG does not intentionally collect file contents or raw code text in telemetry. Before sending telemetry, VibeRAG strips fields that contain file contents/code text and replaces them with summaries (hashes, byte counts, line counts) plus structural metadata (IDs, file paths, line ranges, scores).\n\nQuery text\n\nIn default mode, VibeRAG may collect search query text. VibeRAG applies best-effort redaction of common secret patterns and truncates long strings, but cannot guarantee that all sensitive data is removed. If you work with sensitive data, use stripped or disabled.\n\n4) What error reporting data we collect (Sentry)\n\nWhen enabled, error reports may include:\n\n- exception type/message\n- stack traces\n- basic runtime metadata (OS, Node version, VibeRAG version, service name)\n\nVibeRAG does not intentionally include file contents or code text in error reports.\n\n5) How to opt out\n\n- Disable telemetry and error reporting: /telemetry disabled or VIBERAG_TELEMETRY=disabled\n- You can reset your installation identifier by deleting VibeRAG\u2019s global settings file under VIBERAG_HOME (default: ~/.local/share/viberag/settings.json).\n\n6) Contact\n\nFor questions about telemetry and privacy, open an issue in the VibeRAG repository.\n";
@@ -19,7 +19,7 @@ VibeRAG supports three telemetry modes:
19
19
  - stripped: privacy-preserving telemetry (no query text; minimal metadata).
20
20
  - default: includes query text (with best-effort redaction) and richer structured metadata.
21
21
 
22
- Telemetry is enabled by default. You can change this at any time:
22
+ Telemetry defaults to stripped mode. You can change this at any time:
23
23
 
24
24
  - Run /telemetry disabled|stripped|default in the VibeRAG CLI.
25
25
  - Or set VIBERAG_TELEMETRY=disabled|stripped|default as an environment variable.
@@ -4,6 +4,14 @@
4
4
  * Goal: capture exceptions reliably from CLI/daemon/MCP without leaking file contents.
5
5
  */
6
6
  export type SentryServiceName = 'cli' | 'daemon' | 'mcp';
7
+ export type SentryEventLevel = 'fatal' | 'error' | 'warning' | 'log' | 'info' | 'debug';
8
+ export interface SentryCaptureContext {
9
+ tags?: Record<string, string>;
10
+ extra?: Record<string, unknown>;
11
+ contexts?: Record<string, Record<string, unknown>>;
12
+ fingerprint?: string[];
13
+ level?: SentryEventLevel;
14
+ }
7
15
  export declare function initSentry(args: {
8
16
  service: SentryServiceName;
9
17
  version: string;
@@ -11,8 +19,6 @@ export declare function initSentry(args: {
11
19
  enabled: boolean;
12
20
  shutdown: () => Promise<void>;
13
21
  };
14
- export declare function captureException(error: unknown, context?: {
15
- tags?: Record<string, string>;
16
- extra?: Record<string, unknown>;
17
- }): void;
22
+ export declare function captureException(error: unknown, context?: SentryCaptureContext): void;
23
+ export declare function captureMessage(message: string, context?: SentryCaptureContext): void;
18
24
  export declare function flushSentry(timeoutMs: number): Promise<boolean>;
@@ -20,6 +20,41 @@ function scrubSensitiveText(value) {
20
20
  v = v.replace(/[a-f0-9]{64,}/gi, '[REDACTED_SECRET]');
21
21
  return v;
22
22
  }
23
+ function applyContextToScope(scope, context) {
24
+ if (!context)
25
+ return;
26
+ if (context.tags) {
27
+ for (const [key, value] of Object.entries(context.tags)) {
28
+ scope.setTag(key, value);
29
+ }
30
+ }
31
+ if (context.extra) {
32
+ for (const [key, value] of Object.entries(context.extra)) {
33
+ scope.setExtra(key, value);
34
+ }
35
+ }
36
+ if (context.contexts) {
37
+ for (const [key, value] of Object.entries(context.contexts)) {
38
+ scope.setContext(key, value);
39
+ }
40
+ }
41
+ if (context.fingerprint && context.fingerprint.length > 0) {
42
+ scope.setFingerprint(context.fingerprint);
43
+ }
44
+ if (context.level) {
45
+ scope.setLevel(context.level);
46
+ }
47
+ }
48
+ function withScopedContext(context, fn) {
49
+ if (!context) {
50
+ fn();
51
+ return;
52
+ }
53
+ Sentry.withScope(scope => {
54
+ applyContextToScope(scope, context);
55
+ fn();
56
+ });
57
+ }
23
58
  export function initSentry(args) {
24
59
  const envMode = getTelemetryModeEnvOverride();
25
60
  const effectiveMode = envMode ?? loadUserSettingsSync().telemetry.mode;
@@ -53,6 +88,12 @@ export function initSentry(args) {
53
88
  entry.value = scrubSensitiveText(entry.value);
54
89
  }
55
90
  }
91
+ if (event.message) {
92
+ event.message = scrubSensitiveText(event.message);
93
+ }
94
+ if (event.logentry?.message) {
95
+ event.logentry.message = scrubSensitiveText(event.logentry.message);
96
+ }
56
97
  return event;
57
98
  },
58
99
  });
@@ -70,9 +111,18 @@ export function initSentry(args) {
70
111
  }
71
112
  export function captureException(error, context) {
72
113
  try {
73
- Sentry.captureException(error, {
74
- tags: context?.tags,
75
- extra: context?.extra,
114
+ withScopedContext(context, () => {
115
+ Sentry.captureException(error);
116
+ });
117
+ }
118
+ catch {
119
+ // Ignore
120
+ }
121
+ }
122
+ export function captureMessage(message, context) {
123
+ try {
124
+ withScopedContext(context, () => {
125
+ Sentry.captureMessage(message, context?.level ?? 'error');
76
126
  });
77
127
  }
78
128
  catch {
@@ -10,7 +10,7 @@ import fsSync from 'node:fs';
10
10
  import fs from 'node:fs/promises';
11
11
  import path from 'node:path';
12
12
  import { getUserSettingsPath } from './constants.js';
13
- const DEFAULT_TELEMETRY_MODE = 'default';
13
+ const DEFAULT_TELEMETRY_MODE = 'stripped';
14
14
  function createDefaultUserSettings(now) {
15
15
  return {
16
16
  schemaVersion: 1,
@@ -67,6 +67,15 @@ export interface FailedChunk {
67
67
  files: string[];
68
68
  chunkCount: number;
69
69
  }
70
+ /**
71
+ * Lightweight memory snapshot for status polling/observability.
72
+ */
73
+ export interface DaemonMemorySnapshot {
74
+ rssMB: number;
75
+ heapUsedMB: number;
76
+ externalMB: number;
77
+ arrayBuffersMB: number;
78
+ }
70
79
  /**
71
80
  * Status response for clients.
72
81
  * Enhanced to support polling-based state synchronization.
@@ -83,6 +92,7 @@ export interface DaemonStatus {
83
92
  totalRefs?: number;
84
93
  embeddingProvider?: string;
85
94
  embeddingModel?: string;
95
+ memory: DaemonMemorySnapshot;
86
96
  warmupStatus: string;
87
97
  warmupElapsedMs?: number;
88
98
  warmupCancelRequestedAt?: string | null;
@@ -130,6 +140,7 @@ export declare class DaemonOwner {
130
140
  private warmupStartTime;
131
141
  private warmupAbortController;
132
142
  private indexingAbortController;
143
+ private memoryMonitor;
133
144
  constructor(projectRoot: string);
134
145
  /**
135
146
  * Initialize the daemon owner.
@@ -154,6 +165,18 @@ export declare class DaemonOwner {
154
165
  * Wire indexing service events to daemon state.
155
166
  */
156
167
  private wireIndexingEvents;
168
+ /**
169
+ * Start memory monitoring for production diagnostics.
170
+ */
171
+ private startMemoryMonitor;
172
+ /**
173
+ * Stop memory monitoring.
174
+ */
175
+ private stopMemoryMonitor;
176
+ /**
177
+ * Report a memory monitor diagnostic event to Sentry with daemon context.
178
+ */
179
+ private captureMemoryMonitorEvent;
157
180
  /**
158
181
  * Start warmup in background.
159
182
  */
@@ -14,8 +14,11 @@ import * as crypto from 'node:crypto';
14
14
  import { loadConfig, configExists } from './lib/config.js';
15
15
  import { getDaemonPidPath, getDaemonSocketPath } from './lib/constants.js';
16
16
  import { createServiceLogger } from './lib/logger.js';
17
+ import { captureMessage, flushSentry } from './lib/telemetry/sentry.js';
17
18
  import { isAbortError, throwIfAborted } from './lib/abort.js';
18
19
  import { daemonState } from './state.js';
20
+ import { DaemonMemoryMonitor, } from './services/memory-monitor.js';
21
+ import { buildMemoryMonitorSentryEvent, toSentryCaptureContext, } from './services/memory-monitor-sentry.js';
19
22
  import { SearchEngineV2 } from './services/v2/search/engine.js';
20
23
  import { runV2Eval, } from './services/v2/eval/eval.js';
21
24
  import { IndexingServiceV2 } from './services/v2/indexing.js';
@@ -26,6 +29,36 @@ import { loadV2Manifest, v2ManifestExists } from './services/v2/manifest.js';
26
29
  // Types
27
30
  // ============================================================================
28
31
  const AUTO_INDEX_CANCEL_PAUSE_MS = 30000;
32
+ const MAX_FAILURE_HISTORY = 100;
33
+ const BYTES_PER_MB = 1024 * 1024;
34
+ function toMB(bytes) {
35
+ return Number((bytes / BYTES_PER_MB).toFixed(1));
36
+ }
37
+ function sha256Hex(value) {
38
+ return crypto.createHash('sha256').update(value).digest('hex');
39
+ }
40
+ function parsePositiveEnvNumber(name) {
41
+ const raw = process.env[name]?.trim();
42
+ if (!raw)
43
+ return undefined;
44
+ const value = Number(raw);
45
+ if (!Number.isFinite(value) || value <= 0) {
46
+ return undefined;
47
+ }
48
+ return value;
49
+ }
50
+ function parseEnvMbToBytes(name) {
51
+ const mb = parsePositiveEnvNumber(name);
52
+ if (mb === undefined)
53
+ return undefined;
54
+ return Math.floor(mb * BYTES_PER_MB);
55
+ }
56
+ function parseEnvPositiveInteger(name) {
57
+ const value = parsePositiveEnvNumber(name);
58
+ if (value === undefined)
59
+ return undefined;
60
+ return Math.floor(value);
61
+ }
29
62
  // ============================================================================
30
63
  // Daemon Owner
31
64
  // ============================================================================
@@ -103,6 +136,12 @@ export class DaemonOwner {
103
136
  writable: true,
104
137
  value: null
105
138
  });
139
+ Object.defineProperty(this, "memoryMonitor", {
140
+ enumerable: true,
141
+ configurable: true,
142
+ writable: true,
143
+ value: null
144
+ });
106
145
  this.projectRoot = projectRoot;
107
146
  }
108
147
  // ==========================================================================
@@ -148,6 +187,7 @@ export class DaemonOwner {
148
187
  this.storage = new StorageV2(this.projectRoot, this.config.embeddingDimensions);
149
188
  await this.storage.connect();
150
189
  this.log('info', 'Storage connected');
190
+ await this.startMemoryMonitor();
151
191
  // Start watcher (if enabled)
152
192
  if (this.config.watch?.enabled !== false) {
153
193
  this.watcher = new FileWatcher(this.projectRoot);
@@ -180,6 +220,7 @@ export class DaemonOwner {
180
220
  */
181
221
  async shutdown() {
182
222
  this.log('info', 'Daemon shutting down');
223
+ await this.stopMemoryMonitor();
183
224
  // Stop watcher
184
225
  if (this.watcher) {
185
226
  await this.watcher.stop();
@@ -388,7 +429,7 @@ export class DaemonOwner {
388
429
  indexer.on('slot-failure', ({ batchInfo, error, files, chunkCount }) => {
389
430
  daemonState.update(state => ({
390
431
  failures: [
391
- ...state.failures,
432
+ ...state.failures.slice(-(MAX_FAILURE_HISTORY - 1)),
392
433
  {
393
434
  batchInfo,
394
435
  error,
@@ -412,6 +453,61 @@ export class DaemonOwner {
412
453
  // ==========================================================================
413
454
  // SearchEngine Management (WarmupManager pattern)
414
455
  // ==========================================================================
456
+ /**
457
+ * Start memory monitoring for production diagnostics.
458
+ */
459
+ async startMemoryMonitor() {
460
+ if (this.memoryMonitor)
461
+ return;
462
+ const thresholdBytes = parseEnvMbToBytes('VIBERAG_MEMORY_MONITOR_THRESHOLD_MB');
463
+ let recoveryBytes = parseEnvMbToBytes('VIBERAG_MEMORY_MONITOR_RECOVERY_MB');
464
+ if (thresholdBytes !== undefined &&
465
+ recoveryBytes !== undefined &&
466
+ recoveryBytes >= thresholdBytes) {
467
+ this.log('warn', 'Ignoring VIBERAG_MEMORY_MONITOR_RECOVERY_MB because it must be lower than VIBERAG_MEMORY_MONITOR_THRESHOLD_MB.');
468
+ recoveryBytes = undefined;
469
+ }
470
+ this.memoryMonitor = new DaemonMemoryMonitor({
471
+ projectRoot: this.projectRoot,
472
+ logger: (level, message) => this.log(level, message),
473
+ onReport: report => {
474
+ this.captureMemoryMonitorEvent(report);
475
+ },
476
+ pollIntervalMs: parseEnvPositiveInteger('VIBERAG_MEMORY_MONITOR_POLL_INTERVAL_MS'),
477
+ thresholdBytes,
478
+ recoveryBytes,
479
+ growthThresholdBytes: parseEnvMbToBytes('VIBERAG_MEMORY_MONITOR_GROWTH_THRESHOLD_MB'),
480
+ growthWindowMs: parseEnvPositiveInteger('VIBERAG_MEMORY_MONITOR_GROWTH_WINDOW_MS'),
481
+ minReportIntervalMs: parseEnvPositiveInteger('VIBERAG_MEMORY_MONITOR_MIN_REPORT_INTERVAL_MS'),
482
+ maxReportsPerDay: parseEnvPositiveInteger('VIBERAG_MEMORY_MONITOR_MAX_REPORTS_PER_DAY'),
483
+ });
484
+ await this.memoryMonitor.start();
485
+ }
486
+ /**
487
+ * Stop memory monitoring.
488
+ */
489
+ async stopMemoryMonitor() {
490
+ if (!this.memoryMonitor)
491
+ return;
492
+ await this.memoryMonitor.stop();
493
+ this.memoryMonitor = null;
494
+ }
495
+ /**
496
+ * Report a memory monitor diagnostic event to Sentry with daemon context.
497
+ */
498
+ captureMemoryMonitorEvent(report) {
499
+ const state = daemonState.getSnapshot();
500
+ const watcherStatus = this.getWatcherStatus();
501
+ const event = buildMemoryMonitorSentryEvent({
502
+ report,
503
+ state,
504
+ watcherStatus,
505
+ projectRoot: this.projectRoot,
506
+ });
507
+ captureMessage(event.message, toSentryCaptureContext(event));
508
+ void flushSentry(2000);
509
+ this.log('warn', `Memory monitor triggered Sentry report (${event.triggerSummary}) at rss=${event.rssMB}MB`);
510
+ }
415
511
  /**
416
512
  * Start warmup in background.
417
513
  */
@@ -700,10 +796,18 @@ export class DaemonOwner {
700
796
  const elapsedMs = state.indexing.startedAt
701
797
  ? Math.max(0, now - new Date(state.indexing.startedAt).getTime())
702
798
  : null;
799
+ const usage = process.memoryUsage();
800
+ const memory = {
801
+ rssMB: toMB(usage.rss),
802
+ heapUsedMB: toMB(usage.heapUsed),
803
+ externalMB: toMB(usage.external),
804
+ arrayBuffersMB: toMB(usage.arrayBuffers),
805
+ };
703
806
  const status = {
704
807
  initialized: await configExists(this.projectRoot),
705
808
  indexed: await v2ManifestExists(this.projectRoot),
706
809
  warmupStatus: state.warmup.status,
810
+ memory,
707
811
  warmupElapsedMs,
708
812
  warmupCancelRequestedAt: state.warmup.cancelRequestedAt,
709
813
  warmupCancelledAt: state.warmup.cancelledAt,
@@ -842,5 +946,5 @@ export class DaemonOwner {
842
946
  }
843
947
  }
844
948
  function computeRepoId(projectRoot) {
845
- return crypto.createHash('sha256').update(projectRoot).digest('hex');
949
+ return sha256Hex(projectRoot);
846
950
  }
@@ -0,0 +1,32 @@
1
+ import type { DaemonState } from '../state.js';
2
+ import type { WatcherStatus } from './watcher.js';
3
+ import type { MemoryMonitorReport } from './memory-monitor.js';
4
+ import type { SentryCaptureContext, SentryEventLevel } from '../lib/telemetry/sentry.js';
5
+ export interface MemoryMonitorProcessSnapshot {
6
+ pid: number;
7
+ uptimeSec: number;
8
+ nodeVersion: string;
9
+ platform: string;
10
+ arch: string;
11
+ resourceUsage: NodeJS.ResourceUsage;
12
+ }
13
+ export interface BuildMemoryMonitorSentryEventArgs {
14
+ report: MemoryMonitorReport;
15
+ state: DaemonState;
16
+ watcherStatus: WatcherStatus;
17
+ projectRoot: string;
18
+ nowMs?: number;
19
+ processSnapshot?: MemoryMonitorProcessSnapshot;
20
+ }
21
+ export interface MemoryMonitorSentryEvent {
22
+ message: string;
23
+ level: SentryEventLevel;
24
+ fingerprint: string[];
25
+ tags: Record<string, string>;
26
+ contexts: Record<string, Record<string, unknown>>;
27
+ extra: Record<string, unknown>;
28
+ triggerSummary: string;
29
+ rssMB: number;
30
+ }
31
+ export declare function buildMemoryMonitorSentryEvent(args: BuildMemoryMonitorSentryEventArgs): MemoryMonitorSentryEvent;
32
+ export declare function toSentryCaptureContext(event: MemoryMonitorSentryEvent): SentryCaptureContext;