imsg-mcp 1.0.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.
@@ -0,0 +1,207 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { monitorEventLoopDelay } from "node:perf_hooks";
3
+ import { D as isShuttingDown, w as info, E as warn, r as registerCleanup, F as error, s as shutdown } from "./shutdown-B9ClCyco.js";
4
+ function envNum(name, fallback) {
5
+ const raw = process.env[name];
6
+ if (!raw) return fallback;
7
+ const parsed = Number.parseInt(raw, 10);
8
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
9
+ }
10
+ const EVENT_LOOP_SAMPLE_MS = envNum("IMSG_EVENT_LOOP_SAMPLE_MS", 5e3);
11
+ const EVENT_LOOP_WARN_MS = envNum("IMSG_EVENT_LOOP_WARN_MS", 500);
12
+ const EVENT_LOOP_KILL_MS = envNum("IMSG_EVENT_LOOP_KILL_MS", 1e4);
13
+ const EVENT_LOOP_SUSTAINED_MS = envNum("IMSG_EVENT_LOOP_SUSTAINED_MS", 750);
14
+ const EVENT_LOOP_SUSTAINED_SAMPLES = envNum("IMSG_EVENT_LOOP_SUSTAINED_SAMPLES", 6);
15
+ const MEMORY_SAMPLE_MS = envNum("IMSG_MEMORY_SAMPLE_MS", 6e4);
16
+ const MAX_RSS_MB = envNum("IMSG_MAX_RSS_MB", 1024);
17
+ const MEMORY_GROWTH_SAMPLES = envNum("IMSG_HEAP_GROWTH_SAMPLES", 10);
18
+ const IDLE_RESTART_AFTER_MS = envNum("IMSG_RESTART_AFTER_MS", 24 * 60 * 60 * 1e3);
19
+ const IDLE_RESTART_QUIET_MS = envNum("IMSG_RESTART_QUIET_MS", 60 * 60 * 1e3);
20
+ const IDLE_CHECK_MS = envNum("IMSG_IDLE_CHECK_MS", 10 * 60 * 1e3);
21
+ const state = {
22
+ startedAt: Date.now(),
23
+ eventLoopP99Ms: 0,
24
+ eventLoopMaxMs: 0,
25
+ eventLoopSustainedCount: 0,
26
+ lastEventLoopSampleTs: Date.now(),
27
+ rssMb: 0,
28
+ heapMb: 0,
29
+ heapHistory: [],
30
+ lastActivityTs: Date.now(),
31
+ killReason: null
32
+ };
33
+ let eventLoopHistogram = null;
34
+ let eventLoopTimer = null;
35
+ let memoryTimer = null;
36
+ let idleTimer = null;
37
+ let installed = false;
38
+ function noteActivity() {
39
+ state.lastActivityTs = Date.now();
40
+ }
41
+ function readWatchdogState() {
42
+ return state;
43
+ }
44
+ const memSampleSubscribers = /* @__PURE__ */ new Set();
45
+ function onMemorySample(cb) {
46
+ memSampleSubscribers.add(cb);
47
+ return () => {
48
+ memSampleSubscribers.delete(cb);
49
+ };
50
+ }
51
+ function installWatchdog() {
52
+ if (installed) return;
53
+ installed = true;
54
+ eventLoopHistogram = monitorEventLoopDelay({ resolution: 20 });
55
+ eventLoopHistogram.enable();
56
+ const stateFilePath = process.env.IMSG_WATCHDOG_STATE_PATH ?? "";
57
+ eventLoopTimer = setInterval(() => {
58
+ if (!eventLoopHistogram || isShuttingDown()) return;
59
+ const now = Date.now();
60
+ const interval = now - state.lastEventLoopSampleTs;
61
+ state.lastEventLoopSampleTs = now;
62
+ if (interval > 3 * EVENT_LOOP_SAMPLE_MS) {
63
+ eventLoopHistogram.reset();
64
+ state.eventLoopSustainedCount = 0;
65
+ info("sleep_detected_skipping_sample", {
66
+ actual_interval_ms: interval,
67
+ expected_interval_ms: EVENT_LOOP_SAMPLE_MS
68
+ });
69
+ return;
70
+ }
71
+ const p99Ms = eventLoopHistogram.percentile(99) / 1e6;
72
+ const maxMs = eventLoopHistogram.max / 1e6;
73
+ state.eventLoopP99Ms = p99Ms;
74
+ state.eventLoopMaxMs = maxMs;
75
+ eventLoopHistogram.reset();
76
+ if (stateFilePath) {
77
+ try {
78
+ writeFileSync(
79
+ stateFilePath,
80
+ JSON.stringify({
81
+ ts: Date.now(),
82
+ uptimeMs: Date.now() - state.startedAt,
83
+ eventLoopP99Ms: state.eventLoopP99Ms,
84
+ eventLoopMaxMs: state.eventLoopMaxMs,
85
+ eventLoopSustainedCount: state.eventLoopSustainedCount,
86
+ rssMb: state.rssMb,
87
+ heapMb: state.heapMb,
88
+ killReason: state.killReason
89
+ })
90
+ );
91
+ } catch {
92
+ }
93
+ }
94
+ if (p99Ms >= EVENT_LOOP_KILL_MS) {
95
+ triggerKill("event_loop_blocked", {
96
+ p99_ms: p99Ms,
97
+ max_ms: maxMs,
98
+ threshold_ms: EVENT_LOOP_KILL_MS
99
+ });
100
+ return;
101
+ }
102
+ if (p99Ms >= EVENT_LOOP_SUSTAINED_MS) {
103
+ state.eventLoopSustainedCount += 1;
104
+ if (state.eventLoopSustainedCount >= EVENT_LOOP_SUSTAINED_SAMPLES) {
105
+ triggerKill("event_loop_sustained_lag", {
106
+ p99_ms: p99Ms,
107
+ max_ms: maxMs,
108
+ consecutive_samples: state.eventLoopSustainedCount,
109
+ sample_interval_ms: EVENT_LOOP_SAMPLE_MS,
110
+ sustained_threshold_ms: EVENT_LOOP_SUSTAINED_MS
111
+ });
112
+ return;
113
+ }
114
+ } else {
115
+ state.eventLoopSustainedCount = 0;
116
+ }
117
+ if (p99Ms >= EVENT_LOOP_WARN_MS) {
118
+ warn("event_loop_lag", { p99_ms: p99Ms, max_ms: maxMs, threshold_ms: EVENT_LOOP_WARN_MS });
119
+ }
120
+ }, EVENT_LOOP_SAMPLE_MS);
121
+ eventLoopTimer.unref();
122
+ memoryTimer = setInterval(() => {
123
+ if (isShuttingDown()) return;
124
+ const mu = process.memoryUsage();
125
+ const rssMb = round1(mu.rss / 1024 / 1024);
126
+ const heapMb = round1(mu.heapUsed / 1024 / 1024);
127
+ state.rssMb = rssMb;
128
+ state.heapMb = heapMb;
129
+ for (const cb of memSampleSubscribers) {
130
+ try {
131
+ cb(rssMb, heapMb);
132
+ } catch {
133
+ }
134
+ }
135
+ state.heapHistory.push(heapMb);
136
+ if (state.heapHistory.length > MEMORY_GROWTH_SAMPLES) {
137
+ state.heapHistory.shift();
138
+ }
139
+ if (rssMb >= MAX_RSS_MB) {
140
+ triggerKill("rss_exceeded", { rss_mb: rssMb, threshold_mb: MAX_RSS_MB });
141
+ return;
142
+ }
143
+ if (state.heapHistory.length >= MEMORY_GROWTH_SAMPLES && isMonotonicallyGrowing(state.heapHistory)) {
144
+ triggerKill("memory_leak_suspected", {
145
+ samples: state.heapHistory.slice(),
146
+ sample_interval_ms: MEMORY_SAMPLE_MS
147
+ });
148
+ }
149
+ }, MEMORY_SAMPLE_MS);
150
+ memoryTimer.unref();
151
+ idleTimer = setInterval(() => {
152
+ if (isShuttingDown()) return;
153
+ const uptimeMs = Date.now() - state.startedAt;
154
+ const idleMs = Date.now() - state.lastActivityTs;
155
+ if (uptimeMs >= IDLE_RESTART_AFTER_MS && idleMs >= IDLE_RESTART_QUIET_MS) {
156
+ triggerKill("idle_restart", { uptime_ms: uptimeMs, idle_ms: idleMs });
157
+ }
158
+ }, IDLE_CHECK_MS);
159
+ idleTimer.unref();
160
+ registerCleanup(() => {
161
+ if (eventLoopHistogram) {
162
+ eventLoopHistogram.disable();
163
+ eventLoopHistogram = null;
164
+ }
165
+ if (eventLoopTimer) clearInterval(eventLoopTimer);
166
+ if (memoryTimer) clearInterval(memoryTimer);
167
+ if (idleTimer) clearInterval(idleTimer);
168
+ });
169
+ info("watchdog_installed", {
170
+ event_loop_warn_ms: EVENT_LOOP_WARN_MS,
171
+ event_loop_kill_ms: EVENT_LOOP_KILL_MS,
172
+ event_loop_sustained_ms: EVENT_LOOP_SUSTAINED_MS,
173
+ event_loop_sustained_samples: EVENT_LOOP_SUSTAINED_SAMPLES,
174
+ max_rss_mb: MAX_RSS_MB,
175
+ memory_growth_samples: MEMORY_GROWTH_SAMPLES,
176
+ idle_restart_after_ms: IDLE_RESTART_AFTER_MS
177
+ });
178
+ }
179
+ function round1(n) {
180
+ return Math.round(n * 10) / 10;
181
+ }
182
+ function isMonotonicallyGrowing(samples) {
183
+ if (samples.length < 2) return false;
184
+ let prev = samples[0];
185
+ for (let i = 1; i < samples.length; i++) {
186
+ if (samples[i] < prev) return false;
187
+ prev = samples[i];
188
+ }
189
+ return samples[samples.length - 1] - samples[0] >= 25;
190
+ }
191
+ function triggerKill(reason, data) {
192
+ if (state.killReason) return;
193
+ state.killReason = reason;
194
+ error(`watchdog_kill: ${reason}`, data);
195
+ setTimeout(() => {
196
+ error("watchdog_force_exit — graceful shutdown stalled", { reason });
197
+ process.exit(137);
198
+ }, 5e3).unref();
199
+ shutdown(1).catch(() => process.exit(1));
200
+ }
201
+ export {
202
+ installWatchdog as i,
203
+ noteActivity as n,
204
+ onMemorySample as o,
205
+ readWatchdogState as r
206
+ };
207
+ //# sourceMappingURL=watchdog-V3lgEhMp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watchdog-V3lgEhMp.js","sources":["../src/watchdog.ts"],"sourcesContent":["/**\n * Self-healing watchdog.\n *\n * Three independent monitors run on unref'd timers — they never prevent the\n * process from exiting on their own. When any monitor detects an unrecoverable\n * condition it triggers `shutdown()` so the host (Cursor / Claude / Warp)\n * spawns a clean instance.\n *\n * 1. Event-loop lag monitor (perf_hooks.monitorEventLoopDelay)\n * - warn > EVENT_LOOP_WARN_MS p99 over 5s window\n * - kill > EVENT_LOOP_KILL_MS p99 over 5s window\n *\n * 2. Memory monitor\n * - warn > heap exceeds HEAP_WARN_MB (handled by logger.startHeapMonitor)\n * - kill > RSS exceeds MAX_RSS_MB OR heap monotonically grew on\n * MEMORY_GROWTH_SAMPLES consecutive 60s samples\n *\n * 3. Idle / uptime monitor\n * - kill > uptime > IDLE_RESTART_AFTER_MS AND no activity within\n * IDLE_RESTART_QUIET_MS — graceful restart insurance for crufty\n * long-running processes.\n *\n * All thresholds are configurable via env vars so they can be tuned per\n * environment without rebuilding.\n */\n\nimport { writeFileSync } from \"node:fs\";\nimport { type IntervalHistogram, monitorEventLoopDelay } from \"node:perf_hooks\";\nimport { error, info, warn } from \"./logger.js\";\nimport { isShuttingDown, registerCleanup, shutdown } from \"./shutdown.js\";\n\n// ── Config (env-overridable) ─────────────────────────────────────────────\n\nfunction envNum(name: string, fallback: number): number {\n const raw = process.env[name];\n if (!raw) return fallback;\n const parsed = Number.parseInt(raw, 10);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;\n}\n\nconst EVENT_LOOP_SAMPLE_MS = envNum(\"IMSG_EVENT_LOOP_SAMPLE_MS\", 5_000);\nconst EVENT_LOOP_WARN_MS = envNum(\"IMSG_EVENT_LOOP_WARN_MS\", 500);\nconst EVENT_LOOP_KILL_MS = envNum(\"IMSG_EVENT_LOOP_KILL_MS\", 10_000);\n// Sustained-lag detector: kill if p99 stays >= the sustained threshold for\n// SUSTAINED_SAMPLES consecutive samples. Catches scenarios where the spike\n// kill threshold (10s) is never crossed but the UI is sustained-unusable\n// (e.g. 800ms event-loop lag for several minutes from a render hot-loop).\nconst EVENT_LOOP_SUSTAINED_MS = envNum(\"IMSG_EVENT_LOOP_SUSTAINED_MS\", 750);\nconst EVENT_LOOP_SUSTAINED_SAMPLES = envNum(\"IMSG_EVENT_LOOP_SUSTAINED_SAMPLES\", 6);\n\nconst MEMORY_SAMPLE_MS = envNum(\"IMSG_MEMORY_SAMPLE_MS\", 60_000);\nconst MAX_RSS_MB = envNum(\"IMSG_MAX_RSS_MB\", 1024);\nconst MEMORY_GROWTH_SAMPLES = envNum(\"IMSG_HEAP_GROWTH_SAMPLES\", 10);\n\nconst IDLE_RESTART_AFTER_MS = envNum(\"IMSG_RESTART_AFTER_MS\", 24 * 60 * 60 * 1000); // 24h\nconst IDLE_RESTART_QUIET_MS = envNum(\"IMSG_RESTART_QUIET_MS\", 60 * 60 * 1000); // 1h\nconst IDLE_CHECK_MS = envNum(\"IMSG_IDLE_CHECK_MS\", 10 * 60 * 1000); // 10 min\n\n// ── State ────────────────────────────────────────────────────────────────\n\ninterface WatchdogState {\n startedAt: number;\n eventLoopP99Ms: number;\n eventLoopMaxMs: number;\n /** Consecutive samples where p99 was >= EVENT_LOOP_SUSTAINED_MS. */\n eventLoopSustainedCount: number;\n /** Wall-clock timestamp of the most recent event-loop sample tick.\n * Used to detect system sleep (huge interval gap → reset histogram). */\n lastEventLoopSampleTs: number;\n rssMb: number;\n heapMb: number;\n heapHistory: number[]; // recent heap samples for leak detection\n lastActivityTs: number;\n killReason: string | null;\n}\n\nconst state: WatchdogState = {\n startedAt: Date.now(),\n eventLoopP99Ms: 0,\n eventLoopMaxMs: 0,\n eventLoopSustainedCount: 0,\n lastEventLoopSampleTs: Date.now(),\n rssMb: 0,\n heapMb: 0,\n heapHistory: [],\n lastActivityTs: Date.now(),\n killReason: null,\n};\n\nlet eventLoopHistogram: IntervalHistogram | null = null;\nlet eventLoopTimer: ReturnType<typeof setInterval> | null = null;\nlet memoryTimer: ReturnType<typeof setInterval> | null = null;\nlet idleTimer: ReturnType<typeof setInterval> | null = null;\nlet installed = false;\n\n// ── Public API ───────────────────────────────────────────────────────────\n\n/** Update the activity timestamp — call this from each tool dispatch. */\nexport function noteActivity(): void {\n state.lastActivityTs = Date.now();\n}\n\n/** Read current watchdog state — used by health_check and TUI dev stats. */\nexport function readWatchdogState(): Readonly<WatchdogState> {\n return state;\n}\n\n// ── Memory-pressure subscriber API ───────────────────────────────────────\ntype MemorySampleCallback = (rssMb: number, heapMb: number) => void;\nconst memSampleSubscribers = new Set<MemorySampleCallback>();\n\n/**\n * Subscribe to the watchdog's existing 60s memory sample.\n * Returns an unsubscribe function. Used by the TUI message cache to evict\n * entries under heap pressure without spinning up its own sampler.\n */\nexport function onMemorySample(cb: MemorySampleCallback): () => void {\n memSampleSubscribers.add(cb);\n return () => {\n memSampleSubscribers.delete(cb);\n };\n}\n\n/** Install all three monitors. Idempotent — safe to call multiple times. */\nexport function installWatchdog(): void {\n if (installed) return;\n installed = true;\n\n // 1. Event-loop lag monitor\n eventLoopHistogram = monitorEventLoopDelay({ resolution: 20 });\n eventLoopHistogram.enable();\n\n const stateFilePath = process.env.IMSG_WATCHDOG_STATE_PATH ?? \"\";\n\n eventLoopTimer = setInterval(() => {\n if (!eventLoopHistogram || isShuttingDown()) return;\n\n // Sleep-skew detection: if wall-clock time between this tick and the\n // previous one is much larger than the sample interval, the laptop\n // probably slept (macOS suspends timers but the histogram keeps\n // accumulating). Reset the histogram and skip threshold evaluation —\n // otherwise we'd kill the process for \"event_loop_blocked\" with p99\n // values like 17 minutes that are pure wall-clock skew, not real lag.\n const now = Date.now();\n const interval = now - state.lastEventLoopSampleTs;\n state.lastEventLoopSampleTs = now;\n if (interval > 3 * EVENT_LOOP_SAMPLE_MS) {\n eventLoopHistogram.reset();\n state.eventLoopSustainedCount = 0;\n info(\"sleep_detected_skipping_sample\", {\n actual_interval_ms: interval,\n expected_interval_ms: EVENT_LOOP_SAMPLE_MS,\n });\n return;\n }\n\n // perf_hooks reports nanoseconds — convert to ms.\n const p99Ms = eventLoopHistogram.percentile(99) / 1e6;\n const maxMs = eventLoopHistogram.max / 1e6;\n state.eventLoopP99Ms = p99Ms;\n state.eventLoopMaxMs = maxMs;\n eventLoopHistogram.reset();\n\n // External observer hook: write state to a JSON file each sample tick so\n // a parent process (e.g. the CI stress harness) can read RSS / lag /\n // sustained-lag-count without parsing logs. Best-effort; failures are\n // silent so the watchdog never crashes the process it's supposed to\n // protect.\n if (stateFilePath) {\n try {\n writeFileSync(\n stateFilePath,\n JSON.stringify({\n ts: Date.now(),\n uptimeMs: Date.now() - state.startedAt,\n eventLoopP99Ms: state.eventLoopP99Ms,\n eventLoopMaxMs: state.eventLoopMaxMs,\n eventLoopSustainedCount: state.eventLoopSustainedCount,\n rssMb: state.rssMb,\n heapMb: state.heapMb,\n killReason: state.killReason,\n }),\n );\n } catch {\n // ignore — non-essential\n }\n }\n\n // Single-spike kill: one sample crossing the spike threshold.\n if (p99Ms >= EVENT_LOOP_KILL_MS) {\n triggerKill(\"event_loop_blocked\", {\n p99_ms: p99Ms,\n max_ms: maxMs,\n threshold_ms: EVENT_LOOP_KILL_MS,\n });\n return;\n }\n\n // Sustained-lag kill: many consecutive samples above the sustained\n // threshold. Catches a render-hot-loop pinning the UI without ever\n // reaching the spike threshold.\n if (p99Ms >= EVENT_LOOP_SUSTAINED_MS) {\n state.eventLoopSustainedCount += 1;\n if (state.eventLoopSustainedCount >= EVENT_LOOP_SUSTAINED_SAMPLES) {\n triggerKill(\"event_loop_sustained_lag\", {\n p99_ms: p99Ms,\n max_ms: maxMs,\n consecutive_samples: state.eventLoopSustainedCount,\n sample_interval_ms: EVENT_LOOP_SAMPLE_MS,\n sustained_threshold_ms: EVENT_LOOP_SUSTAINED_MS,\n });\n return;\n }\n } else {\n state.eventLoopSustainedCount = 0;\n }\n\n if (p99Ms >= EVENT_LOOP_WARN_MS) {\n warn(\"event_loop_lag\", { p99_ms: p99Ms, max_ms: maxMs, threshold_ms: EVENT_LOOP_WARN_MS });\n }\n }, EVENT_LOOP_SAMPLE_MS);\n eventLoopTimer.unref();\n\n // 2. Memory monitor — augments logger.ts heap warnings with hard kill rules\n memoryTimer = setInterval(() => {\n if (isShuttingDown()) return;\n const mu = process.memoryUsage();\n const rssMb = round1(mu.rss / 1024 / 1024);\n const heapMb = round1(mu.heapUsed / 1024 / 1024);\n state.rssMb = rssMb;\n state.heapMb = heapMb;\n\n // Notify subscribers (e.g. TUI message cache) so they can evict on pressure\n for (const cb of memSampleSubscribers) {\n try {\n cb(rssMb, heapMb);\n } catch {\n // Subscriber failures must not crash the watchdog\n }\n }\n\n // Track heap history for monotonic growth detection\n state.heapHistory.push(heapMb);\n if (state.heapHistory.length > MEMORY_GROWTH_SAMPLES) {\n state.heapHistory.shift();\n }\n\n if (rssMb >= MAX_RSS_MB) {\n triggerKill(\"rss_exceeded\", { rss_mb: rssMb, threshold_mb: MAX_RSS_MB });\n return;\n }\n\n if (\n state.heapHistory.length >= MEMORY_GROWTH_SAMPLES &&\n isMonotonicallyGrowing(state.heapHistory)\n ) {\n triggerKill(\"memory_leak_suspected\", {\n samples: state.heapHistory.slice(),\n sample_interval_ms: MEMORY_SAMPLE_MS,\n });\n }\n }, MEMORY_SAMPLE_MS);\n memoryTimer.unref();\n\n // 3. Idle / uptime monitor — kill if uptime > N AND no recent activity\n idleTimer = setInterval(() => {\n if (isShuttingDown()) return;\n const uptimeMs = Date.now() - state.startedAt;\n const idleMs = Date.now() - state.lastActivityTs;\n if (uptimeMs >= IDLE_RESTART_AFTER_MS && idleMs >= IDLE_RESTART_QUIET_MS) {\n triggerKill(\"idle_restart\", { uptime_ms: uptimeMs, idle_ms: idleMs });\n }\n }, IDLE_CHECK_MS);\n idleTimer.unref();\n\n registerCleanup(() => {\n if (eventLoopHistogram) {\n eventLoopHistogram.disable();\n eventLoopHistogram = null;\n }\n if (eventLoopTimer) clearInterval(eventLoopTimer);\n if (memoryTimer) clearInterval(memoryTimer);\n if (idleTimer) clearInterval(idleTimer);\n });\n\n info(\"watchdog_installed\", {\n event_loop_warn_ms: EVENT_LOOP_WARN_MS,\n event_loop_kill_ms: EVENT_LOOP_KILL_MS,\n event_loop_sustained_ms: EVENT_LOOP_SUSTAINED_MS,\n event_loop_sustained_samples: EVENT_LOOP_SUSTAINED_SAMPLES,\n max_rss_mb: MAX_RSS_MB,\n memory_growth_samples: MEMORY_GROWTH_SAMPLES,\n idle_restart_after_ms: IDLE_RESTART_AFTER_MS,\n });\n}\n\n// ── Internal helpers ─────────────────────────────────────────────────────\n\nfunction round1(n: number): number {\n return Math.round(n * 10) / 10;\n}\n\n/** Returns true iff every sample is >= the previous (with at least 25MB total growth). */\nexport function isMonotonicallyGrowing(samples: number[]): boolean {\n if (samples.length < 2) return false;\n let prev = samples[0];\n for (let i = 1; i < samples.length; i++) {\n if (samples[i] < prev) return false;\n prev = samples[i];\n }\n // Require at least 25MB total growth to ignore noise. 5MB was too sensitive\n // — innocuous drift triggered false-positive `memory_leak_suspected` kills.\n return samples[samples.length - 1] - samples[0] >= 25;\n}\n\nfunction triggerKill(reason: string, data: Record<string, unknown>): void {\n if (state.killReason) return; // already killing\n state.killReason = reason;\n error(`watchdog_kill: ${reason}`, data);\n // Use shutdown() so registered cleanups run. Force a hard exit if cleanup\n // itself hangs (e.g. SQL is wedged) — 5s grace.\n setTimeout(() => {\n error(\"watchdog_force_exit — graceful shutdown stalled\", { reason });\n process.exit(137);\n }, 5_000).unref();\n shutdown(1).catch(() => process.exit(1));\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ /* auto-generated by NAPI-RS */
2
+ /* eslint-disable */
3
+ /**
4
+ * Get messages for a specific chat, sorted chronologically (oldest first).
5
+ * Returns the last `limit` messages.
6
+ */
7
+ export declare function getMessages(dbPath: string, chatIdentifier: string, limit?: number | undefined | null, includeReactionDetails?: boolean | undefined | null): Promise<Array<NativeMessage>>
8
+
9
+ /**
10
+ * List conversations with metadata, sorted by last message date.
11
+ * Runs entirely on a background thread — returns a JS Promise.
12
+ */
13
+ export declare function listConversations(dbPath: string, contactsMainPath: string, contactsSourcesDir: string | undefined | null, slugsDbPath: string, limit?: number | undefined | null): Promise<Array<NativeConversation>>
14
+
15
+ /** Mirrors the TypeScript `Attachment` interface. */
16
+ export interface NativeAttachment {
17
+ filename: string
18
+ mimeType?: string
19
+ transferName?: string
20
+ totalBytes: number
21
+ }
22
+
23
+ /** Mirrors the TypeScript `Conversation` interface. */
24
+ export interface NativeConversation {
25
+ chatId: string
26
+ chatIdentifier: string
27
+ displayName?: string
28
+ rawIdentifier: string
29
+ participants: Array<string>
30
+ lastMessageDate?: number
31
+ lastMessageSnippet?: string
32
+ unreadCount: number
33
+ threadSlug: string
34
+ isGroupChat: boolean
35
+ serviceType: string
36
+ }
37
+
38
+ /** Mirrors the TypeScript `Message` interface. */
39
+ export interface NativeMessage {
40
+ id: number
41
+ guid: string
42
+ text?: string
43
+ handle: string
44
+ displayName?: string
45
+ isFromMe: boolean
46
+ date: number
47
+ dateRead?: number
48
+ dateDelivered?: number
49
+ isRead: boolean
50
+ isDelivered: boolean
51
+ chatId: string
52
+ service: string
53
+ isReaction: boolean
54
+ isReply: boolean
55
+ replyToText?: string
56
+ replyToGuid?: string
57
+ reactions?: Array<NativeReaction>
58
+ richContentType?: string
59
+ richContentSummary?: string
60
+ isEdited: boolean
61
+ isRetracted: boolean
62
+ hasAttachments: boolean
63
+ attachments?: Array<NativeAttachment>
64
+ }
65
+
66
+ /** Mirrors the TypeScript `Reaction` interface. */
67
+ export interface NativeReaction {
68
+ reactionType: string
69
+ emoji?: string
70
+ fromHandle: string
71
+ isRemoval: boolean
72
+ targetMessageGuid: string
73
+ targetMessagePart: number
74
+ }
75
+
76
+ /**
77
+ * Parse an attributedBody blob to extract readable text.
78
+ * Useful for messages where the `text` column is NULL.
79
+ */
80
+ export declare function parseAttributedBody(blob: Buffer): string | null
81
+
82
+ /**
83
+ * Resolve contact names for a list of handles (phone numbers / emails).
84
+ * Returns a map of handle → display name.
85
+ */
86
+ export declare function resolveContacts(contactsMainPath: string, contactsSourcesDir: string | undefined | null, handles: Array<string>): Promise<Record<string, string>>