Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions main/src/database/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,51 @@ interface ExecutionDiffRow {
timestamp: string;
}

const DEBUG_DB_PANEL_STATE = process.env.PANE_DEBUG_DB_PANEL_STATE === "1";
const LARGE_PANEL_STATE_FIELDS = new Set([
"scrollbackBuffer",
"alternateScreenBuffer",
"serializedBuffer",
"commandHistory",
"lastActiveCommand",
"outputBuffer",
]);

function summarizePanelStateField(value: unknown): string {
if (typeof value === "string") {
return `[string length=${value.length}]`;
}

if (Array.isArray(value)) {
return `[array length=${value.length}]`;
}

if (value && typeof value === "object") {
return `[object keys=${Object.keys(value).length}]`;
}

return `[${typeof value}]`;
}

function sanitizePanelStateForLog(value: unknown): unknown {
if (Array.isArray(value)) {
return value.map(item => sanitizePanelStateForLog(item));
}

if (!value || typeof value !== "object") {
return value;
}

return Object.fromEntries(
Object.entries(value as Record<string, unknown>).map(([key, nestedValue]) => [
key,
LARGE_PANEL_STATE_FIELDS.has(key)
? summarizePanelStateField(nestedValue)
: sanitizePanelStateForLog(nestedValue),
]),
);
}

export class DatabaseService {
private db: Database.Database;

Expand Down Expand Up @@ -3969,6 +4014,15 @@ export class DatabaseService {
}
}

if (DEBUG_DB_PANEL_STATE) {
console.log("[DB-DEBUG] updatePanel state merge:", {
panelId,
updates: sanitizePanelStateForLog(updates.state),
existing: sanitizePanelStateForLog(existingState),
merged: sanitizePanelStateForLog(mergedState),
});
}

setClauses.push("state = ?");
values.push(JSON.stringify(mergedState));
}
Expand Down
12 changes: 12 additions & 0 deletions main/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ import type { Project } from './database/models';
import type { GitStatus } from './types/session';
import { resourceMonitorService } from './services/resourceMonitorService';

function isArchivedSessionOutputValidation(validation: { error?: string; sessionId?: string }): boolean {
return Boolean(
validation.sessionId &&
validation.error === `Session ${validation.sessionId} is archived`
);
}

export function setupEventListeners(services: AppServices, getMainWindow: () => BrowserWindow | null): void {
const {
sessionManager,
Expand Down Expand Up @@ -137,6 +144,11 @@ export function setupEventListeners(services: AppServices, getMainWindow: () =>
// Validate the output has valid session context
const validation = validateEventContext(output);
if (!validation.valid) {
if (isArchivedSessionOutputValidation(validation)) {
console.log(`[Validation] Dropping late session-output for archived session ${validation.sessionId}`);
return;
}

logValidationFailure('session-output event', validation);
return; // Don't broadcast invalid events
}
Expand Down
2 changes: 1 addition & 1 deletion main/src/services/configManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ export class ConfigManager extends EventEmitter {
}

private async saveConfig(): Promise<void> {
const configJson = JSON.stringify(this.config, null, 2);
const writeConfig = async () => {
const configJson = JSON.stringify(this.config, null, 2);
await fs.mkdir(this.configDir, { recursive: true });
const tmpPath = `${this.configPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;

Expand Down
58 changes: 48 additions & 10 deletions main/src/utils/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const ORIGINAL_CONSOLE = {
info: console.info
};

const MAX_LOG_EVENT_CHARS = 16 * 1024;
const MAX_CONSOLE_LOG_EVENT_CHARS = 16 * 1024;
const MAX_FILE_LOG_EVENT_BYTES = 64 * 1024;

function sanitizeLogControls(message: string): string {
let sanitized = '';
Expand All @@ -32,6 +33,26 @@ function sanitizeLogControls(message: string): string {
return sanitized;
}

function truncateToByteLength(value: string, maxBytes: number): string {
if (Buffer.byteLength(value) <= maxBytes) {
return value;
}

let low = 0;
let high = value.length;

while (low < high) {
const mid = Math.ceil((low + high) / 2);
if (Buffer.byteLength(value.slice(0, mid)) <= maxBytes) {
low = mid;
} else {
high = mid - 1;
}
}

return value.slice(0, low);
}

export class Logger {
private logDir: string;
private currentLogFile: string;
Expand Down Expand Up @@ -147,7 +168,7 @@ export class Logger {
}

private writeToFile(logMessage: string) {
const messageWithNewline = logMessage + '\n';
const messageWithNewline = `${this.truncateForFile(logMessage)}\n`;

// Add to queue
this.writeQueue.push({
Expand Down Expand Up @@ -231,26 +252,43 @@ export class Logger {
}
}

private normalizeLogEvent(message: string): string {
private normalizeLogEventForConsole(message: string): string {
const sanitized = sanitizeLogControls(message);

if (sanitized.length <= MAX_LOG_EVENT_CHARS) {
if (sanitized.length <= MAX_CONSOLE_LOG_EVENT_CHARS) {
return sanitized;
}

const omittedChars = sanitized.length - MAX_LOG_EVENT_CHARS;
return `${sanitized.slice(0, MAX_LOG_EVENT_CHARS)} ... [truncated ${omittedChars} chars, original=${sanitized.length}]`;
const omittedChars = sanitized.length - MAX_CONSOLE_LOG_EVENT_CHARS;
return `${sanitized.slice(0, MAX_CONSOLE_LOG_EVENT_CHARS)} ... [truncated ${omittedChars} chars, original=${sanitized.length}]`;
}

private truncateForFile(message: string): string {
const sanitized = sanitizeLogControls(message);
const originalBytes = Buffer.byteLength(sanitized);

if (originalBytes <= MAX_FILE_LOG_EVENT_BYTES) {
return sanitized;
}

const suffix = ` ... [truncated file log event from ${originalBytes} bytes to ${MAX_FILE_LOG_EVENT_BYTES} bytes]`;
const suffixBytes = Buffer.byteLength(suffix);
const targetBytes = Math.max(0, MAX_FILE_LOG_EVENT_BYTES - suffixBytes);
const truncated = truncateToByteLength(sanitized, targetBytes);

return `${truncated}${suffix}`;
}

private log(level: string, message: string, error?: Error) {
const timestamp = formatForDatabase();
const errorInfo = error ? ` Error: ${error.message}\nStack: ${error.stack}` : '';
const fullMessage = this.normalizeLogEvent(`[${timestamp}] ${level}: ${message}${errorInfo}`);
const fullMessage = `[${timestamp}] ${level}: ${message}${errorInfo}`;
const consoleMessage = this.normalizeLogEventForConsole(fullMessage);

// Try to log to console, but handle EPIPE errors gracefully
try {
// Always log to console using the original console method to avoid recursion
this.originalConsole.log(fullMessage);
this.originalConsole.log(consoleMessage);
} catch (consoleError: unknown) {
// If console logging fails (e.g., EPIPE), just write to file
if ((consoleError as NodeJS.ErrnoException)?.code !== 'EPIPE' && !this.isInErrorHandler) {
Expand All @@ -259,9 +297,9 @@ export class Logger {
try {
// For non-EPIPE errors, try to at least write the error to file
// Use a direct write to avoid potential recursion through writeToFile
const errorMessage = `[${timestamp}] ERROR: Failed to write to console: ${(consoleError as Error)?.message || 'Unknown error'}\n`;
const errorMessage = `[${timestamp}] ERROR: Failed to write to console: ${(consoleError as Error)?.message || 'Unknown error'}`;
if (this.logStream && !this.logStream.destroyed) {
this.logStream.write(errorMessage);
this.logStream.write(`${this.truncateForFile(errorMessage)}\n`);
}
} catch {
// Silently fail - we've done our best
Expand Down
Loading