From f032c088c3a0011eae4f2b79a5221031580329b9 Mon Sep 17 00:00:00 2001 From: Kaiyi Date: Tue, 18 Aug 2026 15:47:10 +0800 Subject: [PATCH] fix(agent-core-v2): scope interaction turn-end cancellation to the ending agent turnId is an Agent-scope counter starting at 0 per agent, but cancelPendingForTurn matched pending interactions by turnId alone, so any subagent ending its own turn N cancelled approvals/questions parked by other agents on the same turn number. Match on (turnId, agentId) instead, treating an origin without agentId as main. --- .changeset/fix-cross-agent-approval-cancel.md | 5 ++++ .../agentLifecycle/agentLifecycleService.ts | 2 +- .../src/session/interaction/interaction.ts | 2 +- .../session/interaction/interactionService.ts | 6 +++-- packages/agent-core-v2/test/harness/agent.ts | 6 +++-- .../session/interaction/interaction.test.ts | 25 +++++++++++++++++++ 6 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 .changeset/fix-cross-agent-approval-cancel.md diff --git a/.changeset/fix-cross-agent-approval-cancel.md b/.changeset/fix-cross-agent-approval-cancel.md new file mode 100644 index 000000000..39986a6f9 --- /dev/null +++ b/.changeset/fix-cross-agent-approval-cancel.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix pending approval prompts being dismissed when a background subagent finished its own turn. diff --git a/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycleService.ts b/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycleService.ts index d2fae4f31..c6cb012f3 100644 --- a/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycleService.ts +++ b/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycleService.ts @@ -100,7 +100,7 @@ export class AgentLifecycleService extends Disposable implements IAgentLifecycle if (this.interactionBusDisposables.has(handle.id)) return; const d = handle.accessor .get(IEventBus) - .subscribe(TurnEnded, (e) => this.interaction.cancelPendingForTurn(e.turnId)); + .subscribe(TurnEnded, (e) => this.interaction.cancelPendingForTurn(e.turnId, handle.id)); this.interactionBusDisposables.set(handle.id, d); } diff --git a/packages/agent-core-v2/src/session/interaction/interaction.ts b/packages/agent-core-v2/src/session/interaction/interaction.ts index bd16fde89..35721f89d 100644 --- a/packages/agent-core-v2/src/session/interaction/interaction.ts +++ b/packages/agent-core-v2/src/session/interaction/interaction.ts @@ -40,7 +40,7 @@ export interface ISessionInteractionService { respond(id: string, response: unknown): void; listPending(kind?: InteractionKind): readonly Interaction[]; isRecentlyResolved(id: string): boolean; - cancelPendingForTurn(turnId: number): void; + cancelPendingForTurn(turnId: number, agentId?: string): void; readonly onDidChangePending: Event; readonly onDidResolve: Event; } diff --git a/packages/agent-core-v2/src/session/interaction/interactionService.ts b/packages/agent-core-v2/src/session/interaction/interactionService.ts index 3ea0954d4..b7ca2ea30 100644 --- a/packages/agent-core-v2/src/session/interaction/interactionService.ts +++ b/packages/agent-core-v2/src/session/interaction/interactionService.ts @@ -76,10 +76,12 @@ export class SessionInteractionService extends Service implements ISessionIntera this.states.set(interactionNextIdKey, value); } - cancelPendingForTurn(turnId: number): void { + cancelPendingForTurn(turnId: number, agentId: string = MAIN_AGENT_ID): void { let changed = false; for (const [id, entry] of this.pending) { - if (entry.interaction.origin?.turnId !== turnId) continue; + const origin = entry.interaction.origin; + if (origin?.turnId !== turnId) continue; + if ((origin.agentId ?? MAIN_AGENT_ID) !== agentId) continue; this.pending.delete(id); this.rememberResolved(id); const response = { cancelled: true, reason: 'turn_ended' }; diff --git a/packages/agent-core-v2/test/harness/agent.ts b/packages/agent-core-v2/test/harness/agent.ts index c8e71acbf..0a5b0e897 100644 --- a/packages/agent-core-v2/test/harness/agent.ts +++ b/packages/agent-core-v2/test/harness/agent.ts @@ -2052,9 +2052,11 @@ export class AgentTestContext { : interactions.filter((interaction) => interaction.kind === kind); }, isRecentlyResolved: () => false, - cancelPendingForTurn: (turnId: number) => { + cancelPendingForTurn: (turnId: number, agentId: string = 'main') => { for (const [id, interaction] of pending) { - if (interaction.origin?.turnId === turnId) pending.delete(id); + if (interaction.origin?.turnId !== turnId) continue; + if ((interaction.origin?.agentId ?? 'main') !== agentId) continue; + pending.delete(id); } }, onDidChangePending: Event.None as Event, diff --git a/packages/agent-core-v2/test/session/interaction/interaction.test.ts b/packages/agent-core-v2/test/session/interaction/interaction.test.ts index 2a143a3e5..dd81ad2c7 100644 --- a/packages/agent-core-v2/test/session/interaction/interaction.test.ts +++ b/packages/agent-core-v2/test/session/interaction/interaction.test.ts @@ -215,6 +215,31 @@ describe('SessionInteractionService', () => { expect(svc.listPending()).toHaveLength(1); }); + it('cancelPendingForTurn only cancels interactions from the ending agent, not same-numbered turns of other agents', () => { + const svc = ix.get(ISessionInteractionService); + + svc.enqueue({ id: 'm1', kind: 'approval', payload: {}, origin: { agentId: 'main', turnId: 3 } }); + svc.enqueue({ id: 's1', kind: 'approval', payload: {}, origin: { agentId: 'agent-1', turnId: 3 } }); + + svc.cancelPendingForTurn(3, 'agent-1'); + expect(svc.listPending().map((i) => i.id)).toEqual(['m1']); + + svc.cancelPendingForTurn(3, 'main'); + expect(svc.listPending()).toHaveLength(0); + }); + + it('cancelPendingForTurn treats an interaction without origin agentId as belonging to main', () => { + const svc = ix.get(ISessionInteractionService); + + svc.enqueue({ id: 'a1', kind: 'approval', payload: {}, origin: { turnId: 5 } }); + + svc.cancelPendingForTurn(5, 'agent-1'); + expect(svc.listPending()).toHaveLength(1); + + svc.cancelPendingForTurn(5); + expect(svc.listPending()).toHaveLength(0); + }); + it('request journals an interaction.request op to the origin agent wire', () => { const sub = makeFakeAgent('agent-1'); agents.set('agent-1', sub);