diff --git a/packages/cli/src/nonInteractive/session.test.ts b/packages/cli/src/nonInteractive/session.test.ts index 697db77486d..77aaa26f65e 100644 --- a/packages/cli/src/nonInteractive/session.test.ts +++ b/packages/cli/src/nonInteractive/session.test.ts @@ -77,6 +77,7 @@ function createConfig(overrides: ConfigOverrides = {}): Config { getDebugMode: () => false, getApprovalMode: () => 'auto', getOutputFormat: () => 'stream-json', + getWarnings: () => [], initialize: vi.fn(), waitForMcpReady: vi.fn().mockResolvedValue(undefined), getMonitorRegistry: () => mockMonitorRegistry, @@ -330,6 +331,30 @@ describe('runNonInteractiveStreamJson', () => { expect(mockDispatcher.dispatch).toHaveBeenCalledWith(initRequest); }); + it('writes only warnings produced during deferred initialization to stderr', async () => { + const warnings = ['Warning: already emitted before stream-json startup']; + const stderrWrite = vi.spyOn(process.stderr, 'write').mockReturnValue(true); + config = createConfig({ + getWarnings: () => warnings, + initialize: vi.fn().mockImplementation(async () => { + warnings.push('Warning: emitted during stream-json initialization'); + }), + }); + + mockInputReader.read = async function* () { + yield createControlRequest('initialize'); + }; + + await runNonInteractiveStreamJson(config, ''); + + expect(stderrWrite).toHaveBeenCalledWith( + 'Warning: emitted during stream-json initialization\n', + ); + expect(stderrWrite).not.toHaveBeenCalledWith( + 'Warning: already emitted before stream-json startup\n', + ); + }); + it('processes user message when received as first message', async () => { const userMessage = createUserMessage('Hello world'); diff --git a/packages/cli/src/nonInteractive/session.ts b/packages/cli/src/nonInteractive/session.ts index 82cf64159b6..f2fafd326d6 100644 --- a/packages/cli/src/nonInteractive/session.ts +++ b/packages/cli/src/nonInteractive/session.ts @@ -155,6 +155,10 @@ class Session { debugLogger.debug('[Session] Initializing config'); try { + // gemini.tsx has already emitted warnings known before stream-json + // initialization starts. Keep that snapshot so only warnings produced + // by the deferred initialize() call are written here. + const emittedWarnings = new Set(this.config.getWarnings()); // Bracket `config.initialize()` with the same profiler checkpoints // the non-stream-json branch in `gemini.tsx` uses so the // `config_initialize_dur` derived phase shows up in stream-json @@ -166,6 +170,11 @@ class Session { profileCheckpoint('config_initialize_start'); await this.config.initialize(options); profileCheckpoint('config_initialize_end'); + for (const warning of this.config.getWarnings()) { + if (emittedWarnings.has(warning)) continue; + emittedWarnings.add(warning); + process.stderr.write(`${warning}\n`); + } // Stream-json sessions feed prompts straight to the model after init. // Under progressive MCP availability `initialize()` returns before // MCP servers settle, so we must explicitly await discovery here —