viberag 0.7.1 → 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.
- package/dist/cli/commands/useCommands.js +1 -1
- package/dist/client/types.d.ts +6 -0
- package/dist/daemon/lib/chunker/index.js +32 -20
- package/dist/daemon/lib/telemetry/client.d.ts +1 -1
- package/dist/daemon/lib/telemetry/client.js +1 -1
- package/dist/daemon/lib/telemetry/privacy-policy.d.ts +1 -1
- package/dist/daemon/lib/telemetry/privacy-policy.js +1 -1
- package/dist/daemon/lib/telemetry/sentry.d.ts +10 -4
- package/dist/daemon/lib/telemetry/sentry.js +53 -3
- package/dist/daemon/lib/user-settings.js +1 -1
- package/dist/daemon/owner.d.ts +23 -0
- package/dist/daemon/owner.js +106 -2
- package/dist/daemon/services/memory-monitor-sentry.d.ts +32 -0
- package/dist/daemon/services/memory-monitor-sentry.js +200 -0
- package/dist/daemon/services/memory-monitor.d.ts +136 -0
- package/dist/daemon/services/memory-monitor.js +509 -0
- package/dist/daemon/services/v2/indexing.js +113 -46
- package/dist/daemon/services/v2/search/engine.js +138 -26
- package/dist/daemon/services/v2/storage/index.d.ts +1 -0
- package/dist/daemon/services/v2/storage/index.js +18 -0
- package/dist/mcp/server.js +2 -1
- package/package.json +6 -1
- package/scripts/memory/README.md +172 -0
- package/scripts/memory/isolate-intent-growth.mjs +348 -0
- package/scripts/memory/out/.gitkeep +1 -0
- package/scripts/memory/profile.mjs +188 -0
- package/scripts/memory/stress-search-growth.mjs +560 -0
- package/scripts/memory/watch-reindex-stress.mjs +683 -0
|
@@ -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
|
|
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.`);
|
package/dist/client/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 = '
|
|
13
|
+
const DEFAULT_TELEMETRY_MODE = 'stripped';
|
|
14
14
|
function createDefaultUserSettings(now) {
|
|
15
15
|
return {
|
|
16
16
|
schemaVersion: 1,
|
package/dist/daemon/owner.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/daemon/owner.js
CHANGED
|
@@ -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
|
|
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;
|