From 4240e4ec90491c20454fee5381f269da690e2bf1 Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Tue, 14 Jul 2026 18:40:36 +0800 Subject: [PATCH 1/2] feat(kimi-code): keep print-mode goal runs alive until the goal settles - applyPrintBackgroundPolicy waits for goal continuation turns via a goalActive hook before applying the exit/drain/steer mode, bounded by the wait ceiling - createPrintTurnEndings skips the timeout when the remaining budget is not finite - update the changeset to cover the goal-run lifecycle --- .changeset/align-print-background-policy.md | 2 +- apps/kimi-code/src/cli/v2/run-v2-print.ts | 35 ++++++++++++-- apps/kimi-code/test/cli/run-v2-print.test.ts | 49 ++++++++++++++++++++ 3 files changed, 82 insertions(+), 4 deletions(-) diff --git a/.changeset/align-print-background-policy.md b/.changeset/align-print-background-policy.md index dad3160493..a9bbd568e0 100644 --- a/.changeset/align-print-background-policy.md +++ b/.changeset/align-print-background-policy.md @@ -2,4 +2,4 @@ "@moonshot-ai/kimi-code": patch --- -Align the print-mode background-task policy across engines: `print_background_mode` and `print_max_turns` now take effect for `kimi -p` on the experimental engine, with the same exit / drain / steer semantics and defaults as the default engine. +Align the print-mode run lifecycle across engines: `print_background_mode` and `print_max_turns` now take effect for `kimi -p` on the experimental engine, with the same exit / drain / steer semantics and defaults as the default engine, and `kimi -p "/goal ..."` now stays alive until the goal reaches a terminal state instead of exiting after the first turn. diff --git a/apps/kimi-code/src/cli/v2/run-v2-print.ts b/apps/kimi-code/src/cli/v2/run-v2-print.ts index 5cfbeb1489..2a90f3c903 100644 --- a/apps/kimi-code/src/cli/v2/run-v2-print.ts +++ b/apps/kimi-code/src/cli/v2/run-v2-print.ts @@ -373,6 +373,7 @@ async function runNativeTurn( if (result.type === 'completed') { const configService = app.accessor.get(IConfigService); const taskConfig = resolveAgentTaskConfig(configService); + const goalService = agent.accessor.get(IAgentGoalService); try { await applyPrintBackgroundPolicy({ mode: resolvePrintBackgroundMode(configService), @@ -384,6 +385,7 @@ async function runNativeTurn( skipTurnId: turn.id, warn: (message) => stderr.write(`Warning: ${message}\n`), now: () => Date.now(), + goalActive: () => goalService.getGoal().goal?.status === 'active', }); } catch (error) { // A steered turn that fails fails the run (v1 parity). Anything else @@ -537,9 +539,11 @@ export function createPrintTurnEndings(): PrintTurnEndings & { // oxlint-disable-next-line promise/no-multiple-resolved -- `settled` guards the single resolve; the rule cannot see it resolve(value); }; - const timer = setTimeout(() => { - settle(null); - }, ms); + const timer = Number.isFinite(ms) + ? setTimeout(() => { + settle(null); + }, ms) + : undefined; waiter = settle; }); for (;;) { @@ -571,11 +575,21 @@ export interface PrintBackgroundPolicyInput { readonly skipTurnId: number; readonly warn: (message: string) => void; readonly now: () => number; + /** + * Reports whether an agent goal is still `active`. v2 drives goal + * continuation as new turns (v1 keeps a single turn alive), so a `-p` goal + * run must stay alive until the goal leaves `active`, independent of the + * background policy. + */ + readonly goalActive?: () => boolean; } /** * Apply the print-mode (`kimi -p`) background-task policy after the main turn * completes. Mirrors v1's `Session.handlePrintMainTurnCompleted`: + * - goal : while a goal is `active`, keep waiting for its continuation + * turns (bounded by `ceilingS` as a safety net), regardless of + * the background mode; the goal summary drives the exit code. * - 'exit' : return immediately (default). * - 'drain' : suppress + drain background tasks, then return. * - 'steer' : while background tasks are still pending, stay alive so task @@ -587,6 +601,21 @@ export interface PrintBackgroundPolicyInput { export async function applyPrintBackgroundPolicy( input: PrintBackgroundPolicyInput, ): Promise { + if (input.goalActive !== undefined) { + const goalDeadline = input.now() + input.ceilingS * 1000; + while (input.goalActive()) { + const ended = await input.turnEndings.next( + goalDeadline - input.now(), + input.skipTurnId, + ); + if (ended === null) { + input.warn(`print goal wait ceiling reached (${input.ceilingS}s), finishing`); + return; + } + // A continuation turn that does not complete pauses/blocks the goal, so + // the loop condition exits on the next check. + } + } if (input.mode === 'exit') return; if (input.mode === 'drain') { await input.drain(); diff --git a/apps/kimi-code/test/cli/run-v2-print.test.ts b/apps/kimi-code/test/cli/run-v2-print.test.ts index 6e7178509b..93762ef56b 100644 --- a/apps/kimi-code/test/cli/run-v2-print.test.ts +++ b/apps/kimi-code/test/cli/run-v2-print.test.ts @@ -176,6 +176,55 @@ describe('applyPrintBackgroundPolicy', () => { }), ).rejects.toThrow(PrintSteeredTurnFailedError); }); + + it('waits for goal continuation turns before applying the mode', async () => { + let active = true; + let consumed = 0; + const drain = vi.fn(async () => {}); + await applyPrintBackgroundPolicy({ + mode: 'drain', + ceilingS: 60, + maxTurns: 50, + countPending: () => 0, + drain, + turnEndings: scriptedTurnEndings([ + { event: ending(2), apply: () => { consumed += 1; } }, + { + event: ending(3), + apply: () => { + consumed += 1; + active = false; + }, + }, + ]), + skipTurnId: 1, + warn: () => {}, + now: () => Date.now(), + goalActive: () => active, + }); + // Both continuation turns ended before the mode ('drain') ran. + expect(consumed).toBe(2); + expect(drain).toHaveBeenCalledTimes(1); + }); + + it('warns and returns when the goal wait hits the ceiling', async () => { + const warn = vi.fn(); + await applyPrintBackgroundPolicy({ + mode: 'exit', + ceilingS: 10, + maxTurns: 50, + countPending: () => 0, + drain: async () => {}, + // Empty script: no continuation turn ever ends. + turnEndings: scriptedTurnEndings([]), + skipTurnId: 1, + warn, + now: () => Date.now(), + goalActive: () => true, + }); + expect(warn).toHaveBeenCalledTimes(1); + expect(warn.mock.calls[0]?.[0]).toContain('goal wait ceiling'); + }); }); describe('createPrintTurnEndings', () => { From ed507641790967bb98ac996f6f273f6d4aa6ff09 Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Tue, 14 Jul 2026 18:54:24 +0800 Subject: [PATCH 2/2] fix(kimi-code): wake the print goal wait periodically so settled goals exit promptly --- apps/kimi-code/src/cli/v2/run-v2-print.ts | 10 ++++-- apps/kimi-code/test/cli/run-v2-print.test.ts | 37 ++++++++++++++++++-- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/apps/kimi-code/src/cli/v2/run-v2-print.ts b/apps/kimi-code/src/cli/v2/run-v2-print.ts index 2a90f3c903..4e2aa229f3 100644 --- a/apps/kimi-code/src/cli/v2/run-v2-print.ts +++ b/apps/kimi-code/src/cli/v2/run-v2-print.ts @@ -86,6 +86,8 @@ import { const PROMPT_UI_MODE = 'print'; const DEFAULT_PRINT_WAIT_CEILING_S = 3600; const DEFAULT_PRINT_MAX_TURNS = 50; +/** Re-check `goalActive` at least this often while waiting for goal turns. */ +const GOAL_WAIT_POLL_MS = 250; export async function runV2Print( opts: CLIOptions, @@ -604,11 +606,15 @@ export async function applyPrintBackgroundPolicy( if (input.goalActive !== undefined) { const goalDeadline = input.now() + input.ceilingS * 1000; while (input.goalActive()) { + // Also wake on a short poll: a goal can leave `active` without any + // further turn.ended (budget block at a turn boundary, or a pause after + // a continuation-launch failure), which would otherwise hang the run + // until the ceiling. const ended = await input.turnEndings.next( - goalDeadline - input.now(), + Math.min(goalDeadline - input.now(), GOAL_WAIT_POLL_MS), input.skipTurnId, ); - if (ended === null) { + if (ended === null && input.now() >= goalDeadline) { input.warn(`print goal wait ceiling reached (${input.ceilingS}s), finishing`); return; } diff --git a/apps/kimi-code/test/cli/run-v2-print.test.ts b/apps/kimi-code/test/cli/run-v2-print.test.ts index 93762ef56b..2e93c2137a 100644 --- a/apps/kimi-code/test/cli/run-v2-print.test.ts +++ b/apps/kimi-code/test/cli/run-v2-print.test.ts @@ -208,6 +208,7 @@ describe('applyPrintBackgroundPolicy', () => { }); it('warns and returns when the goal wait hits the ceiling', async () => { + let now = 0; const warn = vi.fn(); await applyPrintBackgroundPolicy({ mode: 'exit', @@ -215,16 +216,46 @@ describe('applyPrintBackgroundPolicy', () => { maxTurns: 50, countPending: () => 0, drain: async () => {}, - // Empty script: no continuation turn ever ends. - turnEndings: scriptedTurnEndings([]), + // No continuation turn ever ends; the poll interval elapses each time. + turnEndings: { + next: async () => { + now = 10_001; + return null; + }, + }, skipTurnId: 1, warn, - now: () => Date.now(), + now: () => now, goalActive: () => true, }); expect(warn).toHaveBeenCalledTimes(1); expect(warn.mock.calls[0]?.[0]).toContain('goal wait ceiling'); }); + + it('exits the goal wait promptly when the goal settles without a turn ending', async () => { + let active = true; + const warn = vi.fn(); + await applyPrintBackgroundPolicy({ + mode: 'exit', + ceilingS: 3600, + maxTurns: 50, + countPending: () => 0, + drain: async () => {}, + // Poll interval elapses; the goal settles (paused/blocked) mid-wait + // without producing a turn.ended. + turnEndings: { + next: async () => { + active = false; + return null; + }, + }, + skipTurnId: 1, + warn, + now: () => Date.now(), + goalActive: () => active, + }); + expect(warn).not.toHaveBeenCalled(); + }); }); describe('createPrintTurnEndings', () => {