diff --git a/.changeset/print-wait-background-subagents.md b/.changeset/print-wait-background-subagents.md new file mode 100644 index 000000000..825356473 --- /dev/null +++ b/.changeset/print-wait-background-subagents.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +In `kimi -p` runs, wait for background subagents to finish before exiting when `background.keep_alive_on_exit` is enabled. Set `keep_alive_on_exit = true` to let concurrent background subagents complete. diff --git a/apps/kimi-code/src/cli/run-prompt.ts b/apps/kimi-code/src/cli/run-prompt.ts index 6f9379259..2d2c71b2d 100644 --- a/apps/kimi-code/src/cli/run-prompt.ts +++ b/apps/kimi-code/src/cli/run-prompt.ts @@ -510,7 +510,19 @@ function runPromptTurn( return; case 'turn.ended': if (event.reason === 'completed') { - finish(); + void (async () => { + // Flush the buffered assistant message before draining background + // tasks: in stream-json mode the final message is only emitted by + // finish(), so a long background wait would otherwise withhold the + // main turn's result until the drain settles. + outputWriter.flushAssistant(); + try { + await session.waitForBackgroundTasksOnPrint(); + } catch (error) { + log.warn('waitForBackgroundTasksOnPrint failed', { error }); + } + finish(); + })(); return; } finish(new Error(formatTurnEndedFailure(event))); diff --git a/apps/kimi-code/test/cli/run-prompt.test.ts b/apps/kimi-code/test/cli/run-prompt.test.ts index 41673cc51..924fcdad8 100644 --- a/apps/kimi-code/test/cli/run-prompt.test.ts +++ b/apps/kimi-code/test/cli/run-prompt.test.ts @@ -39,6 +39,7 @@ const mocks = vi.hoisted(() => { handler(mainEvent({ type: 'turn.ended', turnId: 1, reason: 'completed' })); } }), + waitForBackgroundTasksOnPrint: vi.fn(async () => {}), }; return { @@ -659,6 +660,37 @@ describe('runPrompt', () => { ); }); + it('flushes stream-json assistant output before waiting for background tasks', async () => { + let releaseWait: () => void = () => {}; + const waitGate = new Promise((resolve) => { + releaseWait = resolve; + }); + mocks.session.waitForBackgroundTasksOnPrint.mockImplementationOnce(async () => waitGate); + + mocks.session.prompt.mockImplementationOnce(async () => { + for (const handler of mocks.eventHandlers) { + handler(mocks.mainEvent({ type: 'turn.started', turnId: 9, origin: { kind: 'user' } })); + handler(mocks.mainEvent({ type: 'assistant.delta', turnId: 9, delta: 'final answer' })); + handler(mocks.mainEvent({ type: 'turn.ended', turnId: 9, reason: 'completed' })); + } + }); + + const stdout = writer(); + const stderr = writer(); + const runPromise = runPrompt(opts({ outputFormat: 'stream-json' }), '1.2.3-test', { + stdout, + stderr, + }); + + // The assistant message must be flushed even while the background wait is pending. + await waitForAssertion(() => { + expect(stdout.text()).toContain('{"role":"assistant","content":"final answer"}'); + }); + + releaseWait(); + await runPromise; + }); + it('resumes a concrete session without a configured default model', async () => { mocks.harnessGetConfig.mockResolvedValueOnce({ providers: {}, telemetry: true }); mocks.session.getStatus.mockResolvedValueOnce({ permission: 'manual', model: 'saved-model' }); diff --git a/docs/en/configuration/config-files.md b/docs/en/configuration/config-files.md index 254d34b7d..0d4e57a18 100644 --- a/docs/en/configuration/config-files.md +++ b/docs/en/configuration/config-files.md @@ -193,10 +193,13 @@ You can also switch models temporarily without touching the config file — by s | Field | Type | Default | Description | | --- | --- | --- | --- | | `max_running_tasks` | `integer` | — | Maximum number of background tasks running concurrently | -| `keep_alive_on_exit` | `boolean` | `false` | Whether to keep still-running background tasks when the session closes. By default, Kimi Code requests that all background tasks stop before the process exits; set this to `true` only when you want tasks to outlive the session | +| `keep_alive_on_exit` | `boolean` | `false` | Whether to keep still-running background tasks when the session closes. By default, Kimi Code requests that all background tasks stop before the process exits; set this to `true` only when you want tasks to outlive the session. In print mode (`kimi -p`), setting this to `true` also makes the process wait for all background tasks to finish before exiting, so background subagents can complete their work | +| `print_wait_ceiling_s` | `integer` | `3600` | In print mode (`kimi -p`) with `keep_alive_on_exit = true`, the maximum number of seconds the process waits for background tasks to finish after the main agent's turn ends. Has no effect outside print mode or when `keep_alive_on_exit` is `false` | `keep_alive_on_exit` can be overridden by the `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` environment variable, which takes higher priority than `config.toml`. +In print mode (`kimi -p ""`), Kimi Code runs a single non-interactive turn and exits as soon as the main agent finishes. If you launch background tasks (for example, concurrent subagents via `Agent(run_in_background=true)`) and need them to run to completion, set `keep_alive_on_exit = true`: the process then waits for every background task to reach a terminal state before exiting, bounded by `print_wait_ceiling_s`. Without it, the single turn ending tears background tasks down with the process. +