From 6b222abf88eaf735df443e6af9e8682893e9ebb9 Mon Sep 17 00:00:00 2001 From: Tarik Ermis Date: Sun, 9 Aug 2026 08:52:38 +0200 Subject: [PATCH 1/2] fix(agent-core-v2): emit subagent.failed when background task registration fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Agent tool announces a background subagent through subagent.spawned before registering it as a background task. When registration hit the running-task limit, the tool aborted the run and returned the limit error, but the abort surfaced in mirrorAgentRun as an abort error — which deliberately never publishes subagent.failed — so clients that tracked the run from spawned/started (task panels, the TUI background agent badge) kept the rejected launch as a ghost running entry that could never be stopped (the engine never created the task). Publish subagent.failed from the registration-failure path so the spawned run always reaches a terminal signal, via a shared emitAgentRunFailed helper now also used by mirrorAgentRun. --- .../fix-background-subagent-ghost-tasks.md | 5 ++++ .../src/agent/tools/agent/agentTool.ts | 29 +++++++++++++------ .../src/session/subagent/mirrorAgentRun.ts | 18 ++++++++---- packages/agent-core-v2/test/tool/tool.test.ts | 17 +++++++++++ 4 files changed, 55 insertions(+), 14 deletions(-) create mode 100644 .changeset/fix-background-subagent-ghost-tasks.md diff --git a/.changeset/fix-background-subagent-ghost-tasks.md b/.changeset/fix-background-subagent-ghost-tasks.md new file mode 100644 index 0000000000..818b8a9bd5 --- /dev/null +++ b/.changeset/fix-background-subagent-ghost-tasks.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix background subagents rejected by the running-task limit lingering as permanent "running" entries in the task panel. diff --git a/packages/agent-core-v2/src/agent/tools/agent/agentTool.ts b/packages/agent-core-v2/src/agent/tools/agent/agentTool.ts index 330549a1f5..6ca178ed7f 100644 --- a/packages/agent-core-v2/src/agent/tools/agent/agentTool.ts +++ b/packages/agent-core-v2/src/agent/tools/agent/agentTool.ts @@ -75,6 +75,7 @@ import { } from '#/app/agentProfileCatalog/profile-shared'; import { ILogService } from '#/_base/log/log'; import { IConfigService } from '#/app/config/config'; +import { IEventBus } from '#/app/event/eventBus'; import { IFlagService } from '#/app/flag/flag'; import { IModelCatalog } from '#/kosong/model/catalog'; import { IAgentLifecycleService } from '#/session/agentLifecycle/agentLifecycle'; @@ -83,7 +84,11 @@ import { ISessionProcessRunner } from '#/session/process/processRunner'; import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata'; import { ISessionWorkspaceContext } from '#/session/workspaceContext/workspaceContext'; -import { emitAgentRunSpawned, mirrorAgentRun } from '#/session/subagent/mirrorAgentRun'; +import { + emitAgentRunFailed, + emitAgentRunSpawned, + mirrorAgentRun, +} from '#/session/subagent/mirrorAgentRun'; import { ISessionSubagentService } from '#/session/subagent/subagent'; import { buildSubagentModelDescriptions, @@ -474,14 +479,13 @@ export class SubagentTool implements ISubagentTool { subagentType: handle.profileName, error, }); - const message = error instanceof Error ? error.message : String(error); - return { - output: - isError2(error) && error.code === ErrorCodes.TASK_LIMIT_EXCEEDED - ? 'Too many background tasks are already running.' - : message, - isError: true, - }; + const message = registrationFailureMessage(error); + emitAgentRunFailed( + this.lifecycle.get(this.callerAgentId)?.accessor.get(IEventBus), + handle.agentId, + message, + ); + return { output: message, isError: true }; } if (runInBackground) { @@ -635,6 +639,13 @@ function launchErrorMessage(error: unknown, signal: AbortSignal): string { return error instanceof Error ? error.message : String(error); } +function registrationFailureMessage(error: unknown): string { + if (isError2(error) && error.code === ErrorCodes.TASK_LIMIT_EXCEEDED) { + return 'Too many background tasks are already running.'; + } + return error instanceof Error ? error.message : String(error); +} + function formatSubagentStoppedMessage(reason: string | undefined): string { const normalized = reason?.trim(); if (normalized === userCancellationReason().message) return USER_INTERRUPTED_SUBAGENT_MESSAGE; diff --git a/packages/agent-core-v2/src/session/subagent/mirrorAgentRun.ts b/packages/agent-core-v2/src/session/subagent/mirrorAgentRun.ts index 095a1cb705..19a10b955b 100644 --- a/packages/agent-core-v2/src/session/subagent/mirrorAgentRun.ts +++ b/packages/agent-core-v2/src/session/subagent/mirrorAgentRun.ts @@ -129,6 +129,18 @@ export function emitAgentRunSpawned( }); } +export function emitAgentRunFailed( + eventBus: IEventBus | undefined, + targetAgentId: string, + error: string, +): void { + eventBus?.publish({ + type: 'subagent.failed', + subagentId: targetAgentId, + error, + }); +} + export async function mirrorAgentRun( requester: IAgentScopeHandle, run: AgentRunHandle, @@ -174,11 +186,7 @@ export async function mirrorAgentRun( return result; } catch (error) { if (!isAbortError(error) && !shouldSuppressFailure(options, error)) { - eventBus?.publish({ - type: 'subagent.failed', - subagentId: run.agentId, - error: errorMessage(error), - }); + emitAgentRunFailed(eventBus, run.agentId, errorMessage(error)); } throw error; } diff --git a/packages/agent-core-v2/test/tool/tool.test.ts b/packages/agent-core-v2/test/tool/tool.test.ts index 14709c15a2..ccba279f1b 100644 --- a/packages/agent-core-v2/test/tool/tool.test.ts +++ b/packages/agent-core-v2/test/tool/tool.test.ts @@ -1748,6 +1748,23 @@ describe('Agent tool execution contract', () => { output: 'Too many background tasks are already running.', }); expect(lifecycle.create).toHaveBeenCalledTimes(2); + + // The spawn was already announced, so clients tracking the run (task + // panels, the TUI background-agent badge) need a terminal signal — + // without one the rejected launch lingers as a ghost "running" entry. + const events = lifecycle.publishedEvents; + const spawnedIndex = events.findIndex( + (event) => event.type === 'subagent.spawned' && event.subagentId === 'agent-second', + ); + const failedEvents = events.filter( + (event) => event.type === 'subagent.failed' && event.subagentId === 'agent-second', + ); + expect(spawnedIndex).toBeGreaterThanOrEqual(0); + expect(failedEvents).toHaveLength(1); + expect(events.indexOf(failedEvents[0]!)).toBeGreaterThan(spawnedIndex); + expect(failedEvents[0]).toMatchObject({ + error: 'Too many background tasks are already running.', + }); completions[0]?.resolve({ summary: 'finished later' }); }); From c59da2ace989d913f88bb1bb2083d5a1d6f6e9b0 Mon Sep 17 00:00:00 2001 From: Tarik Ermis Date: Sun, 9 Aug 2026 11:00:22 +0200 Subject: [PATCH 2/2] test(agent-core-v2): drop statement-level comment from task-limit test The package convention keeps comments solely in the top-of-file header block; the assertion sequence is self-explanatory without it. --- packages/agent-core-v2/test/tool/tool.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/agent-core-v2/test/tool/tool.test.ts b/packages/agent-core-v2/test/tool/tool.test.ts index ccba279f1b..46618e9987 100644 --- a/packages/agent-core-v2/test/tool/tool.test.ts +++ b/packages/agent-core-v2/test/tool/tool.test.ts @@ -1749,9 +1749,6 @@ describe('Agent tool execution contract', () => { }); expect(lifecycle.create).toHaveBeenCalledTimes(2); - // The spawn was already announced, so clients tracking the run (task - // panels, the TUI background-agent badge) need a terminal signal — - // without one the rejected launch lingers as a ghost "running" entry. const events = lifecycle.publishedEvents; const spawnedIndex = events.findIndex( (event) => event.type === 'subagent.spawned' && event.subagentId === 'agent-second',