diff --git a/cli/src/codex/utils/appServerEventConverter.test.ts b/cli/src/codex/utils/appServerEventConverter.test.ts index c1064bba..88d0a7c4 100644 --- a/cli/src/codex/utils/appServerEventConverter.test.ts +++ b/cli/src/codex/utils/appServerEventConverter.test.ts @@ -1,4 +1,5 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; +import { logger } from '@/ui/logger'; import { AppServerEventConverter } from './appServerEventConverter'; describe('AppServerEventConverter', () => { @@ -742,4 +743,27 @@ describe('AppServerEventConverter', () => { ]); }); + it('truncates large unhandled notification payloads before logging', () => { + const converter = new AppServerEventConverter(); + const debug = vi.spyOn(logger, 'debug').mockImplementation(() => undefined); + const largeImageResult = 'a'.repeat(4096); + + const events = converter.handleNotification('item/completed', { + item: { + id: 'image-1', + type: 'imageGeneration', + result: largeImageResult, + savedPath: '/tmp/image.png' + } + }); + + expect(events).toEqual([]); + expect(debug).toHaveBeenCalledTimes(1); + const logged = debug.mock.calls[0]?.[1] as { params?: { item?: { result?: string; savedPath?: string } } }; + expect(logged.params?.item?.result).not.toBe(largeImageResult); + expect(logged.params?.item?.result).toContain('[truncated 3584 chars for logs]'); + expect(logged.params?.item?.savedPath).toBe('/tmp/image.png'); + + debug.mockRestore(); + }); }); diff --git a/cli/src/codex/utils/appServerEventConverter.ts b/cli/src/codex/utils/appServerEventConverter.ts index da3a1a19..57094ed6 100644 --- a/cli/src/codex/utils/appServerEventConverter.ts +++ b/cli/src/codex/utils/appServerEventConverter.ts @@ -228,6 +228,43 @@ function addEventScope(events: ConvertedEvent[], scope: Record) })); } +const MAX_UNHANDLED_LOG_STRING_LENGTH = 512; +const MAX_UNHANDLED_LOG_ARRAY_LENGTH = 20; +const MAX_UNHANDLED_LOG_DEPTH = 8; + +function sanitizeUnhandledNotificationLogValue(value: unknown, depth: number = 0): unknown { + if (typeof value === 'string') { + if (value.length <= MAX_UNHANDLED_LOG_STRING_LENGTH) { + return value; + } + return `${value.slice(0, MAX_UNHANDLED_LOG_STRING_LENGTH)}... [truncated ${value.length - MAX_UNHANDLED_LOG_STRING_LENGTH} chars for logs]`; + } + + if (Array.isArray(value)) { + const items = value + .slice(0, MAX_UNHANDLED_LOG_ARRAY_LENGTH) + .map((item) => sanitizeUnhandledNotificationLogValue(item, depth + 1)); + if (value.length > MAX_UNHANDLED_LOG_ARRAY_LENGTH) { + items.push(`... [truncated ${value.length - MAX_UNHANDLED_LOG_ARRAY_LENGTH} array items for logs]`); + } + return items; + } + + if (!value || typeof value !== 'object') { + return value; + } + + if (depth >= MAX_UNHANDLED_LOG_DEPTH) { + return '[truncated nested object for logs]'; + } + + const result: Record = {}; + for (const [key, nestedValue] of Object.entries(value)) { + result[key] = sanitizeUnhandledNotificationLogValue(nestedValue, depth + 1); + } + return result; +} + function normalizeCollabAgentToolName(value: unknown): string | null { const raw = asString(value); if (!raw) return null; @@ -899,7 +936,7 @@ export class AppServerEventConverter { } } - logger.debug('[AppServerEventConverter] Unhandled notification', { method, params }); + logger.debug('[AppServerEventConverter] Unhandled notification', sanitizeUnhandledNotificationLogValue({ method, params })); return events; }