Improve attachment memory safety, downloads, and high-memory alert UX.
All checks were successful
Queue Release Build / prepare (push) Successful in 20s
Deploy Web Apps / deploy (push) Successful in 9m2s
Queue Release Build / build-windows (push) Successful in 28m8s
Queue Release Build / build-linux (push) Successful in 47m26s
Queue Release Build / build-android (push) Successful in 19m52s
Queue Release Build / finalize (push) Successful in 4m42s

Stream large receives to disk with chunk acks to cap renderer RAM, evict
off-screen display blobs, and route exports through a disk-aware download
service. Fix the high-memory dialog (backdrop dismiss, copy, log actions),
allow diagnostics paths in the path jail, and restore persisted image
hydration after reload.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-14 00:25:22 +02:00
parent f0d79aa627
commit bb0ac930ad
69 changed files with 2306 additions and 430 deletions

View File

@@ -25,7 +25,9 @@ import { startIdleMonitor, stopIdleMonitor } from '../idle/idle-monitor';
import {
attachRendererDiagnosticsHooks,
ensurePerfDiagIpcRegistered,
shutdownHighMemoryMonitoring,
shutdownPerfDiagnostics,
startHighMemoryMonitoring,
startPerfDiagnostics
} from '../diagnostics';
@@ -39,6 +41,7 @@ function startLocalApiAfterWindowReady(): void {
export function registerAppLifecycle(): void {
ensurePerfDiagIpcRegistered();
startHighMemoryMonitoring();
app.whenReady().then(async () => {
const dockIconPath = getDockIconPath();
@@ -83,6 +86,7 @@ export function registerAppLifecycle(): void {
app.on('before-quit', async (event) => {
prepareWindowForAppQuit();
shutdownHighMemoryMonitoring();
await shutdownPerfDiagnostics();
if (getDataSource()?.isInitialized) {

View File

@@ -10,19 +10,21 @@ import { resolveReadablePath } from '../path-jail';
import { sumWorkingSetKb } from './process-metrics.rules';
import { isPerfDiagEnabled } from './diagnostics.flags';
import { exceedsHighMemoryThreshold } from './high-memory-alert.rules';
import { buildHighMemoryDiagnosticPayload } from './high-memory-snapshot.rules';
import { collectImmediateRendererSamples } from './immediate-renderer-samples.collector';
import { captureHighMemoryDiagnostics } from './high-memory-capture';
import { collectSessionContext } from './session-context.collector';
import {
clearHighMemoryAlert,
readHighMemoryAlert,
writeHighMemoryAlert
writeHighMemoryAlert,
type HighMemoryAlertRecord
} from './high-memory-alert.store';
import type { PerfDiagEntry } from './diagnostics.models';
import { PerfDiagWriter } from './diagnostics.writer';
const PROCESS_POLL_INTERVAL_MS = 5_000;
export const HIGH_MEMORY_ALERT_PENDING_CHANNEL = 'high-memory-alert-pending';
let activeWriter: PerfDiagWriter | null = null;
let processPollTimer: NodeJS.Timeout | null = null;
let diagnosticsEnabled = false;
@@ -67,6 +69,24 @@ export function ensurePerfDiagIpcRegistered(): void {
return true;
});
ipcMain.handle('export-high-memory-diagnostics', async () => {
const metrics = collectAppMetricsSnapshot();
const totalKb = sumWorkingSetKb(metrics.processes) ?? 0;
const record = await captureHighMemoryDiagnostics({
userDataPath: app.getPath('userData'),
sessionStartedAt,
metrics,
totalWorkingSetKb: totalKb,
writer: activeWriter,
mainWindow: getMainWindow(),
reason: 'manual'
});
await persistAndNotifyHighMemoryAlert(record);
return record;
});
ipcMain.handle('show-log-file-in-folder', async (_event, filePath: string) => {
if (typeof filePath !== 'string' || !filePath.trim()) {
return {
@@ -94,8 +114,48 @@ export function getActivePerfDiagWriter(): PerfDiagWriter | null {
return activeWriter;
}
export function startHighMemoryMonitoring(): void {
ensurePerfDiagIpcRegistered();
if (!sessionStartedAt) {
sessionStartedAt = Date.now();
highMemoryAlertTriggeredThisSession = false;
}
if (processPollTimer) {
return;
}
const sample = (): void => {
try {
const metrics = collectAppMetricsSnapshot();
const totalKb = sumWorkingSetKb(metrics.processes);
if (activeWriter && diagnosticsEnabled) {
activeWriter.append({
collectedAt: metrics.collectedAt,
source: 'main',
type: 'process',
payload: {
totalWorkingSetKb: totalKb,
processes: metrics.processes
}
});
}
void maybeTriggerHighMemoryAlert(metrics, totalKb);
} catch {
// Collector failures must never affect the app.
}
};
sample();
processPollTimer = setInterval(sample, PROCESS_POLL_INTERVAL_MS);
}
export function startPerfDiagnostics(): PerfDiagWriter | null {
ensurePerfDiagIpcRegistered();
startHighMemoryMonitoring();
diagnosticsEnabled = isPerfDiagEnabled(process.env, app.isPackaged);
if (!diagnosticsEnabled) {
@@ -109,10 +169,7 @@ export function startPerfDiagnostics(): PerfDiagWriter | null {
});
activeWriter = writer;
highMemoryAlertTriggeredThisSession = false;
sessionStartedAt = Date.now();
registerProcessCrashHandlers(writer);
startProcessMetricsPolling(writer);
const userDataPath = app.getPath('userData');
@@ -188,14 +245,15 @@ export async function shutdownPerfDiagnostics(): Promise<void> {
}
await activeWriter.flushSnapshot('shutdown');
activeWriter = null;
diagnosticsEnabled = false;
}
export function shutdownHighMemoryMonitoring(): void {
if (processPollTimer) {
clearInterval(processPollTimer);
processPollTimer = null;
}
activeWriter = null;
diagnosticsEnabled = false;
}
function registerProcessCrashHandlers(writer: PerfDiagWriter): void {
@@ -241,34 +299,7 @@ function registerProcessCrashHandlers(writer: PerfDiagWriter): void {
});
}
function startProcessMetricsPolling(writer: PerfDiagWriter): void {
const sample = (): void => {
try {
const metrics = collectAppMetricsSnapshot();
const totalKb = sumWorkingSetKb(metrics.processes);
writer.append({
collectedAt: metrics.collectedAt,
source: 'main',
type: 'process',
payload: {
totalWorkingSetKb: totalKb,
processes: metrics.processes
}
});
void maybeTriggerHighMemoryAlert(writer, metrics, totalKb);
} catch {
// Collector failures must never affect the app.
}
};
sample();
processPollTimer = setInterval(sample, PROCESS_POLL_INTERVAL_MS);
}
async function maybeTriggerHighMemoryAlert(
writer: PerfDiagWriter,
metrics: AppMetricsSnapshot,
totalWorkingSetKb: number | null
): Promise<void> {
@@ -278,51 +309,26 @@ async function maybeTriggerHighMemoryAlert(
highMemoryAlertTriggeredThisSession = true;
const detectedAt = Date.now();
const userDataPath = app.getPath('userData');
const immediateRendererEntries = await collectImmediateRendererSamples(getMainWindow());
const environment = collectSessionContext({
const record = await captureHighMemoryDiagnostics({
userDataPath: app.getPath('userData'),
sessionStartedAt,
userDataPath
metrics,
totalWorkingSetKb: totalWorkingSetKb ?? 0,
writer: activeWriter,
mainWindow: getMainWindow(),
reason: 'threshold'
});
for (const entry of immediateRendererEntries) {
writer.append(entry);
}
await persistAndNotifyHighMemoryAlert(record);
}
writer.append({
collectedAt: detectedAt,
source: 'main',
type: 'environment',
payload: {
...environment
}
});
async function persistAndNotifyHighMemoryAlert(record: HighMemoryAlertRecord): Promise<void> {
await writeHighMemoryAlert(app.getPath('userData'), record);
notifyHighMemoryAlert(record);
}
writer.append({
collectedAt: detectedAt,
source: 'main',
type: 'high-memory',
payload: buildHighMemoryDiagnosticPayload({
detectedAt,
totalWorkingSetKb: totalWorkingSetKb ?? 0,
metrics,
environment,
mainProcessMemory: process.memoryUsage(),
ringEntries: writer.bufferedEntries,
immediateRendererEntries,
sessionId: writer.sessionId
})
});
await writer.flushSnapshot('high-memory-threshold');
await writeHighMemoryAlert(userDataPath, {
logFilePath: writer.snapshotFilePath,
detectedAt,
peakWorkingSetKb: totalWorkingSetKb ?? 0,
sessionId: writer.sessionId
});
function notifyHighMemoryAlert(record: HighMemoryAlertRecord): void {
getMainWindow()?.webContents.send(HIGH_MEMORY_ALERT_PENDING_CHANNEL, record);
}
function normalizeRendererEntry(entry: PerfDiagEntry): PerfDiagEntry {

View File

@@ -33,7 +33,8 @@ describe('high-memory-alert.store', () => {
logFilePath: path.join(userDataPath, 'diagnostics', 'perf-session.jsonl'),
detectedAt: 1_700_000_000_000,
peakWorkingSetKb: 2_200_000,
sessionId: 'session-1'
sessionId: 'session-1',
reason: 'threshold' as const
};
await writeHighMemoryAlert(userDataPath, record);

View File

@@ -1,11 +1,14 @@
import * as fsp from 'fs/promises';
import * as path from 'path';
export type HighMemoryAlertReason = 'manual' | 'threshold';
export interface HighMemoryAlertRecord {
logFilePath: string;
detectedAt: number;
peakWorkingSetKb: number;
sessionId: string;
reason?: HighMemoryAlertReason;
}
export function resolveHighMemoryAlertPath(userDataPath: string): string {
@@ -31,7 +34,10 @@ export async function readHighMemoryAlert(userDataPath: string): Promise<HighMem
logFilePath: parsed.logFilePath,
detectedAt: parsed.detectedAt,
peakWorkingSetKb: parsed.peakWorkingSetKb,
sessionId: parsed.sessionId
sessionId: parsed.sessionId,
...(parsed.reason === 'manual' || parsed.reason === 'threshold'
? { reason: parsed.reason }
: {})
};
} catch {
return null;

View File

@@ -0,0 +1,57 @@
import {
beforeEach,
describe,
expect,
it,
vi
} from 'vitest';
import * as os from 'os';
import * as path from 'path';
import * as fsp from 'fs/promises';
import { captureHighMemoryDiagnostics } from './high-memory-capture';
vi.mock('./immediate-renderer-samples.collector', () => ({
collectImmediateRendererSamples: vi.fn(async () => [])
}));
vi.mock('./session-context.collector', () => ({
collectSessionContext: vi.fn(() => ({
platform: 'linux',
userDataPath: '/tmp/user-data'
}))
}));
describe('captureHighMemoryDiagnostics', () => {
let userDataPath = '';
beforeEach(async () => {
userDataPath = await fsp.mkdtemp(path.join(os.tmpdir(), 'metoyou-high-memory-capture-'));
});
it('writes a diagnostics snapshot and returns an alert record', async () => {
const record = await captureHighMemoryDiagnostics({
userDataPath,
sessionStartedAt: Date.now() - 60_000,
metrics: {
collectedAt: Date.now(),
processes: [
{
pid: 1,
type: 'Browser',
workingSetKb: 2_200_000
}
]
},
totalWorkingSetKb: 2_200_000,
writer: null,
mainWindow: null,
reason: 'manual'
});
expect(record.peakWorkingSetKb).toBe(2_200_000);
expect(record.reason).toBe('manual');
expect(record.logFilePath).toContain(userDataPath);
await expect(fsp.stat(record.logFilePath)).resolves.toBeDefined();
});
});

View File

@@ -0,0 +1,80 @@
import type { BrowserWindow } from 'electron';
import type { AppMetricsSnapshot } from '../app-metrics';
import { buildHighMemoryDiagnosticPayload } from './high-memory-snapshot.rules';
import { collectImmediateRendererSamples } from './immediate-renderer-samples.collector';
import { collectSessionContext } from './session-context.collector';
import type { HighMemoryAlertRecord } from './high-memory-alert.store';
import type { PerfDiagEntry } from './diagnostics.models';
import { PerfDiagWriter } from './diagnostics.writer';
export type HighMemoryCaptureReason = 'manual' | 'threshold';
export interface CaptureHighMemoryDiagnosticsInput {
userDataPath: string;
sessionStartedAt: number;
metrics: AppMetricsSnapshot;
totalWorkingSetKb: number;
writer: PerfDiagWriter | null;
mainWindow: BrowserWindow | null;
reason: HighMemoryCaptureReason;
}
export async function captureHighMemoryDiagnostics(
input: CaptureHighMemoryDiagnosticsInput
): Promise<HighMemoryAlertRecord> {
const detectedAt = Date.now();
const writer = input.writer ?? new PerfDiagWriter({
userDataPath: input.userDataPath,
sessionId: `${input.reason}-${detectedAt.toString(36)}-${process.pid}`
});
const immediateRendererEntries = await collectImmediateRendererSamples(input.mainWindow);
const environment = collectSessionContext({
sessionStartedAt: input.sessionStartedAt,
userDataPath: input.userDataPath
});
appendEntries(writer, immediateRendererEntries);
appendEntries(writer, [
{
collectedAt: detectedAt,
source: 'main',
type: 'environment',
payload: {
...environment
}
},
{
collectedAt: detectedAt,
source: 'main',
type: 'high-memory',
payload: buildHighMemoryDiagnosticPayload({
detectedAt,
totalWorkingSetKb: input.totalWorkingSetKb,
metrics: input.metrics,
environment,
mainProcessMemory: process.memoryUsage(),
ringEntries: writer.bufferedEntries,
immediateRendererEntries,
sessionId: writer.sessionId
})
}
]);
await writer.flushSnapshot(
input.reason === 'manual' ? 'manual-export' : 'high-memory-threshold'
);
return {
logFilePath: writer.snapshotFilePath,
detectedAt,
peakWorkingSetKb: input.totalWorkingSetKb,
sessionId: writer.sessionId,
reason: input.reason
};
}
function appendEntries(writer: PerfDiagWriter, entries: readonly PerfDiagEntry[]): void {
for (const entry of entries) {
writer.append(entry);
}
}

View File

@@ -15,8 +15,11 @@ export {
attachRendererDiagnosticsHooks,
ensurePerfDiagIpcRegistered,
getActivePerfDiagWriter,
HIGH_MEMORY_ALERT_PENDING_CHANNEL,
isPerfDiagActive,
shutdownHighMemoryMonitoring,
shutdownPerfDiagnostics,
startHighMemoryMonitoring,
startPerfDiagnostics
} from './diagnostics.lifecycle';
export type { PerfDiagEntry, PerfDiagEntryType, PerfDiagSource } from './diagnostics.models';

View File

@@ -0,0 +1,16 @@
import {
describe,
expect,
it
} from 'vitest';
import { isReadableRegularFile } from './file-read.rules';
describe('file-read.rules', () => {
it('accepts regular files', () => {
expect(isReadableRegularFile({ isFile: () => true })).toBe(true);
});
it('rejects directories and other non-file paths', () => {
expect(isReadableRegularFile({ isFile: () => false })).toBe(false);
});
});

View File

@@ -0,0 +1,6 @@
import type { Stats } from 'fs';
/** Only regular files can be read through the read-file IPC surface. */
export function isReadableRegularFile(stats: Pick<Stats, 'isFile'>): boolean {
return stats.isFile();
}

View File

@@ -68,6 +68,7 @@ import {
grantPluginReadRoot,
resolveReadablePath
} from '../path-jail';
import { isReadableRegularFile } from './file-read.rules';
const DEFAULT_MIME_TYPE = 'application/octet-stream';
const MAX_ACTIVE_DESKTOP_NOTIFICATIONS = 20;
@@ -654,9 +655,19 @@ export function setupSystemHandlers(): void {
return null;
}
const data = await fsp.readFile(scopedPath);
try {
const stats = await fsp.stat(scopedPath);
return data.toString('base64');
if (!isReadableRegularFile(stats)) {
return null;
}
const data = await fsp.readFile(scopedPath);
return data.toString('base64');
} catch {
return null;
}
});
ipcMain.handle('read-file-chunk', async (_event, filePath: string, start: number, end: number) => {
@@ -666,17 +677,27 @@ export function setupSystemHandlers(): void {
return null;
}
const fileHandle = await fsp.open(scopedPath, 'r');
try {
const safeStart = Math.max(0, Math.trunc(start));
const safeEnd = Math.max(safeStart, Math.trunc(end));
const buffer = Buffer.alloc(safeEnd - safeStart);
const result = await fileHandle.read(buffer, 0, buffer.length, safeStart);
const stats = await fsp.stat(scopedPath);
return buffer.subarray(0, result.bytesRead).toString('base64');
} finally {
await fileHandle.close();
if (!isReadableRegularFile(stats)) {
return null;
}
const fileHandle = await fsp.open(scopedPath, 'r');
try {
const safeStart = Math.max(0, Math.trunc(start));
const safeEnd = Math.max(safeStart, Math.trunc(end));
const buffer = Buffer.alloc(safeEnd - safeStart);
const result = await fileHandle.read(buffer, 0, buffer.length, safeStart);
return buffer.subarray(0, result.bytesRead).toString('base64');
} finally {
await fileHandle.close();
}
} catch {
return null;
}
});
@@ -728,6 +749,17 @@ export function setupSystemHandlers(): void {
return true;
});
ipcMain.handle('append-file-bytes', async (_event, filePath: string, bytes: Uint8Array) => {
const scopedPath = await resolveWritableUserDataFilePath(filePath);
if (!scopedPath) {
return false;
}
await fsp.appendFile(scopedPath, Buffer.from(bytes));
return true;
});
ipcMain.handle('delete-file', async (_event, filePath: string) => {
const scopedPath = await resolveWritableUserDataFilePath(filePath);

View File

@@ -35,6 +35,17 @@ describe('path-jail', () => {
await expect(assertPathUnderRoot(tempRoot, allowedPath, ['server'])).resolves.toBe(allowedPath);
});
it('accepts diagnostics log paths under diagnostics', async () => {
const diagnosticsDir = path.join(tempRoot, 'diagnostics');
fs.mkdirSync(diagnosticsDir, { recursive: true });
const logPath = path.join(diagnosticsDir, 'perf-session.jsonl');
fs.writeFileSync(logPath, '{}');
await expect(assertPathUnderRoot(tempRoot, logPath)).resolves.toBe(logPath);
});
it('accepts cached plugin bundle paths under plugin-bundles', async () => {
const bundleDir = path.join(tempRoot, 'plugin-bundles', 'example.plugin', '1.0.0');

View File

@@ -9,7 +9,8 @@ export const DEFAULT_USER_DATA_SUBDIRS = [
'plugin-bundles',
'plugin-cache',
'themes',
'metoyou'
'metoyou',
'diagnostics'
] as const;
export function isPathInside(parentPath: string, candidatePath: string): boolean {

View File

@@ -11,6 +11,7 @@ const AUTO_UPDATE_STATE_CHANGED_CHANNEL = 'auto-update-state-changed';
const DEEP_LINK_RECEIVED_CHANNEL = 'deep-link-received';
const WINDOW_STATE_CHANGED_CHANNEL = 'window-state-changed';
const IDLE_STATE_CHANGED_CHANNEL = 'idle-state-changed';
const HIGH_MEMORY_ALERT_PENDING_CHANNEL = 'high-memory-alert-pending';
export interface LinuxScreenShareAudioRoutingInfo {
available: boolean;
@@ -264,8 +265,23 @@ export interface ElectronAPI {
detectedAt: number;
peakWorkingSetKb: number;
sessionId: string;
reason?: 'manual' | 'threshold';
} | null>;
acknowledgeHighMemoryAlert: () => Promise<boolean>;
exportHighMemoryDiagnostics: () => Promise<{
logFilePath: string;
detectedAt: number;
peakWorkingSetKb: number;
sessionId: string;
reason?: 'manual' | 'threshold';
}>;
onHighMemoryAlertPending: (listener: (alert: {
logFilePath: string;
detectedAt: number;
peakWorkingSetKb: number;
sessionId: string;
reason?: 'manual' | 'threshold';
}) => void) => () => void;
showLogFileInFolder: (filePath: string) => Promise<{ shown: boolean; reason?: string }>;
getAppDataPath: () => Promise<string>;
openCurrentDataFolder: () => Promise<boolean>;
@@ -335,6 +351,7 @@ export interface ElectronAPI {
grantPluginReadRoot: (rootPath: string) => Promise<boolean>;
writeFile: (filePath: string, data: string) => Promise<boolean>;
appendFile: (filePath: string, data: string) => Promise<boolean>;
appendFileBytes: (filePath: string, data: Uint8Array) => Promise<boolean>;
saveFileAs: (defaultFileName: string, data: string) => Promise<{ saved: boolean; cancelled: boolean }>;
saveExistingFileAs: (sourceFilePath: string, defaultFileName: string) => Promise<{ saved: boolean; cancelled: boolean }>;
openFilePath: (filePath: string) => Promise<{ opened: boolean; reason?: string }>;
@@ -410,6 +427,23 @@ const electronAPI: ElectronAPI = {
reportPerfDiagSample: (entry) => ipcRenderer.invoke('perf-diag-report', entry),
getPendingHighMemoryAlert: () => ipcRenderer.invoke('get-pending-high-memory-alert'),
acknowledgeHighMemoryAlert: () => ipcRenderer.invoke('acknowledge-high-memory-alert'),
exportHighMemoryDiagnostics: () => ipcRenderer.invoke('export-high-memory-diagnostics'),
onHighMemoryAlertPending: (listener) => {
const wrappedListener = (_event: Electron.IpcRendererEvent, alert: {
logFilePath: string;
detectedAt: number;
peakWorkingSetKb: number;
sessionId: string;
}) => {
listener(alert);
};
ipcRenderer.on(HIGH_MEMORY_ALERT_PENDING_CHANNEL, wrappedListener);
return () => {
ipcRenderer.removeListener(HIGH_MEMORY_ALERT_PENDING_CHANNEL, wrappedListener);
};
},
showLogFileInFolder: (filePath) => ipcRenderer.invoke('show-log-file-in-folder', filePath),
getAppDataPath: () => ipcRenderer.invoke('get-app-data-path'),
openCurrentDataFolder: () => ipcRenderer.invoke('open-current-data-folder'),
@@ -478,6 +512,7 @@ const electronAPI: ElectronAPI = {
grantPluginReadRoot: (rootPath) => ipcRenderer.invoke('grant-plugin-read-root', rootPath),
writeFile: (filePath, data) => ipcRenderer.invoke('write-file', filePath, data),
appendFile: (filePath, data) => ipcRenderer.invoke('append-file', filePath, data),
appendFileBytes: (filePath, data) => ipcRenderer.invoke('append-file-bytes', filePath, data),
saveFileAs: (defaultFileName, data) => ipcRenderer.invoke('save-file-as', defaultFileName, data),
saveExistingFileAs: (sourceFilePath, defaultFileName) => ipcRenderer.invoke('save-existing-file-as', sourceFilePath, defaultFileName),
openFilePath: (filePath) => ipcRenderer.invoke('open-file-path', filePath),