From 268d028b8f6de5c97e0ed9512f8b26bcdff8712e Mon Sep 17 00:00:00 2001 From: LoG1331 Date: Tue, 11 Aug 2026 04:10:14 +0000 Subject: [PATCH 1/2] fix(goal): stop the daemon crashing when pausing a goal with a queued continuation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aborting a pending goal continuation rejects the bookkeeping promise chain with an AbortError; with no catch attached, the rejection reached the telemetry unhandledRejection handler, which rethrows when it is the sole listener (print/server modes) — killing the kimi web process. Swallow the rejection in the bookkeeping chain (turn failures surface via turn.ended events) and never rethrow AbortError-shaped rejections from the telemetry handler. --- .changeset/goal-pause-daemon-crash.md | 5 +++++ .../agent-core-v2/src/agent/goal/goalService.ts | 13 ++++++++++--- packages/telemetry/src/crash.ts | 10 ++++++---- 3 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 .changeset/goal-pause-daemon-crash.md diff --git a/.changeset/goal-pause-daemon-crash.md b/.changeset/goal-pause-daemon-crash.md new file mode 100644 index 0000000000..6bf289e045 --- /dev/null +++ b/.changeset/goal-pause-daemon-crash.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix the `kimi web` server process exiting when a goal with a queued continuation is paused. diff --git a/packages/agent-core-v2/src/agent/goal/goalService.ts b/packages/agent-core-v2/src/agent/goal/goalService.ts index ae56369e30..30ee981ae6 100644 --- a/packages/agent-core-v2/src/agent/goal/goalService.ts +++ b/packages/agent-core-v2/src/agent/goal/goalService.ts @@ -82,7 +82,7 @@ import { IWireService } from '#/wire/wire'; import { defineModel } from '#/wire/model'; import { IEventBus } from '#/app/event/eventBus'; -import { IAgentGoalService, type GoalReasonInput, type ResumeGoalInput } from './goal'; +import { IAgentGoalService, type GoalReasonInput, type PauseGoalOptions, type ResumeGoalInput } from './goal'; import { IGoalDeadlineScheduler } from './goalDeadlineScheduler'; import { clearGoal, createGoal, GoalModel, updateGoal, type GoalState } from './goalOps'; import type { @@ -544,7 +544,11 @@ export class AgentGoalService extends Disposable implements IAgentGoalService { this.clearInternal('system'); } - async pauseGoal(input: GoalReasonInput = {}, actor: GoalActor = 'user'): Promise { + async pauseGoal( + input: GoalReasonInput = {}, + actor: GoalActor = 'user', + opts: PauseGoalOptions = {}, + ): Promise { this.assertSupportedAgent(); const state = this.requireState(); if (state.status === 'paused') return this.toSnapshot(state); @@ -554,7 +558,9 @@ export class AgentGoalService extends Disposable implements IAgentGoalService { `Cannot pause a goal in status "${state.status}"`, ); } - return this.applyLifecycle(state, 'paused', input.reason, actor); + return this.applyLifecycle(state, 'paused', input.reason, actor, { + preserveLiveContinuation: opts.preserveLiveContinuation === true, + }); } async pauseActiveGoal( @@ -951,6 +957,7 @@ export class AgentGoalService extends Disposable implements IAgentGoalService { } return turn.result; }) + .catch(() => {}) .finally(() => { if (pending.turnId !== undefined) this.pendingContinuationGoals.delete(pending.turnId); if (this.pendingContinuation === pending) this.pendingContinuation = undefined; diff --git a/packages/telemetry/src/crash.ts b/packages/telemetry/src/crash.ts index 35674a834d..a5f6c2f2ec 100644 --- a/packages/telemetry/src/crash.ts +++ b/packages/telemetry/src/crash.ts @@ -55,11 +55,13 @@ export function installCrashHandlersForClient(client: TelemetryClient): () => vo // the only listener (print / server modes) rethrow to preserve it; the // dedupe set above keeps the monitor from double-reporting that path. installedRejectionHandler = (reason: unknown) => { + // AbortError rejections are expected cancellation noise (e.g. a goal's + // pending continuation aborted by pauseGoal) — never track or rethrow + // them, or the daemon dies on a perfectly normal cancel. + if (isAbortError(reason)) return; const soleListener = process.listenerCount('unhandledRejection') === 1; - if (!isAbortError(reason)) { - trackCrash(crashErrorType(reason), 'unhandledRejection'); - recordedRejections.add(reason); - } + trackCrash(crashErrorType(reason), 'unhandledRejection'); + recordedRejections.add(reason); if (soleListener) { throw reason; } From ef0da5c63a07c9bb0138debe4e6d84bdc6ec30d3 Mon Sep 17 00:00:00 2001 From: LoG1331 Date: Tue, 11 Aug 2026 04:10:24 +0000 Subject: [PATCH 2/2] feat(goal): add graceful goal pause that lets the live turn finish Pausing a goal interrupts the in-flight goal turn. The engine already has the preserveLiveContinuation flag (used for blocked/complete transitions); expose it through pauseGoal and the session profile API as goal_control: "pause_graceful" so API clients (e.g. the web UI) can pause without cutting the running turn off mid-tool. --- packages/agent-core-v2/src/agent/goal/goal.ts | 10 +++++++++- .../src/app/sessionLegacy/sessionLegacyService.ts | 3 +++ .../src/app/sessionLegacy/sessionProtocol.ts | 2 +- packages/agent-core-v2/test/agent/goal/goal.test.ts | 9 +++++++++ packages/kap-server/src/protocol/rest-prompt.ts | 2 +- 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/agent-core-v2/src/agent/goal/goal.ts b/packages/agent-core-v2/src/agent/goal/goal.ts index e88c5952c1..224e873f0c 100644 --- a/packages/agent-core-v2/src/agent/goal/goal.ts +++ b/packages/agent-core-v2/src/agent/goal/goal.ts @@ -18,6 +18,10 @@ export interface GoalReasonInput { readonly reason?: string; } +export interface PauseGoalOptions { + readonly preserveLiveContinuation?: boolean; +} + export interface ResumeGoalInput extends GoalReasonInput { readonly continueIfPaused?: boolean; readonly continueIfBlocked?: boolean; @@ -29,7 +33,11 @@ export interface IAgentGoalService { getGoal(): GoalToolResult; isGoalToolTarget(turnId: number, goalId: string): boolean; createGoal(input: CreateGoalInput, actor?: GoalActor): Promise; - pauseGoal(input?: GoalReasonInput, actor?: GoalActor): Promise; + pauseGoal( + input?: GoalReasonInput, + actor?: GoalActor, + opts?: PauseGoalOptions, + ): Promise; resumeGoal(input?: ResumeGoalInput, actor?: GoalActor): Promise; cancelGoal(input?: GoalReasonInput, actor?: GoalActor): Promise; setBudgetLimits( diff --git a/packages/agent-core-v2/src/app/sessionLegacy/sessionLegacyService.ts b/packages/agent-core-v2/src/app/sessionLegacy/sessionLegacyService.ts index 1ec77c346e..4bad9c7aac 100644 --- a/packages/agent-core-v2/src/app/sessionLegacy/sessionLegacyService.ts +++ b/packages/agent-core-v2/src/app/sessionLegacy/sessionLegacyService.ts @@ -142,6 +142,9 @@ export class SessionLegacyService implements ISessionLegacyService { case 'pause': await goal.pauseGoal({}); break; + case 'pause_graceful': + await goal.pauseGoal({}, 'user', { preserveLiveContinuation: true }); + break; case 'resume': await goal.resumeGoal({ continueIfPaused: true, continueIfBlocked: true }); break; diff --git a/packages/agent-core-v2/src/app/sessionLegacy/sessionProtocol.ts b/packages/agent-core-v2/src/app/sessionLegacy/sessionProtocol.ts index b1c0410168..89fcc07507 100644 --- a/packages/agent-core-v2/src/app/sessionLegacy/sessionProtocol.ts +++ b/packages/agent-core-v2/src/app/sessionLegacy/sessionProtocol.ts @@ -46,7 +46,7 @@ export const sessionAgentConfigSchema = z.object({ plan_mode: z.boolean().optional(), swarm_mode: z.boolean().optional(), goal_objective: z.string().optional(), - goal_control: z.enum(['pause', 'resume', 'cancel']).optional(), + goal_control: z.enum(['pause', 'pause_graceful', 'resume', 'cancel']).optional(), }); export type SessionAgentConfig = z.infer; diff --git a/packages/agent-core-v2/test/agent/goal/goal.test.ts b/packages/agent-core-v2/test/agent/goal/goal.test.ts index 07232ac97a..6cdb86dbb8 100644 --- a/packages/agent-core-v2/test/agent/goal/goal.test.ts +++ b/packages/agent-core-v2/test/agent/goal/goal.test.ts @@ -946,6 +946,15 @@ describe('AgentGoalService core workflow hooks', () => { expect(abort).toHaveBeenCalledOnce(); }); + it('keeps the live continuation running on a graceful pause', async () => { + const abort = await startLiveContinuation(); + + await goals.pauseGoal({}, 'user', { preserveLiveContinuation: true }); + + expect(abort).not.toHaveBeenCalled(); + expect(goals.getGoal().goal?.status).toBe('paused'); + }); + it('aborts a live continuation when the user cancels the goal', async () => { const abort = await startLiveContinuation(); diff --git a/packages/kap-server/src/protocol/rest-prompt.ts b/packages/kap-server/src/protocol/rest-prompt.ts index b04a2fe78a..f9e9b2a513 100644 --- a/packages/kap-server/src/protocol/rest-prompt.ts +++ b/packages/kap-server/src/protocol/rest-prompt.ts @@ -42,7 +42,7 @@ export const promptSubmissionSchema = z.object({ plan_mode: z.boolean().optional(), swarm_mode: z.boolean().optional(), goal_objective: z.string().optional(), - goal_control: z.enum(['pause', 'resume', 'cancel']).optional(), + goal_control: z.enum(['pause', 'pause_graceful', 'resume', 'cancel']).optional(), // Client-managed session tool denylist: full-replace on every submit; the // bound profile's own deny always survives. Omit to keep the persisted // value, send `[]` to clear the client portion.