From 8956084e77c691f2585d16c08380a3d02a4e045d Mon Sep 17 00:00:00 2001 From: Kaiyi Date: Mon, 24 Aug 2026 22:18:46 +0800 Subject: [PATCH] fix(flow): abort queued flow prompts when the flow flag turns off --- .../src/features/flow/flowService.ts | 12 ++++++++++-- .../test/features/flow/flowService.test.ts | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/agent-core-v2/src/features/flow/flowService.ts b/packages/agent-core-v2/src/features/flow/flowService.ts index 617b9fbfe..6830c8474 100644 --- a/packages/agent-core-v2/src/features/flow/flowService.ts +++ b/packages/agent-core-v2/src/features/flow/flowService.ts @@ -6,7 +6,8 @@ import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory' import { ScopeActivation, registerScopedService } from '#/_base/di/scope'; import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode'; import { IAgentRuntimeService } from '#/agent/runtimeBinding/agentRuntime'; -import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext'; +import { IAgentScopeContext, agentContextOfScope } from '#/agent/scopeContext/scopeContext'; +import { AgentSkill } from '#/features/skill/skillAgentRuntime'; import { SkillActivated } from '#/features/skill/skillOps'; import { ISkillActivationDataService } from '#/features/skill/skillActivationData'; import { ContextUndone } from '#/agent/undo/undoService'; @@ -24,6 +25,7 @@ import { IFlagService } from '#/app/flag/flag'; import { LifecycleScope } from '#/app/scopes'; import { IEventDispatcher } from '#/state/eventDispatcher'; import type { DeepReadonly } from '#/state/state'; +import { IAgentLifecycleService } from '#/session/agentLifecycle/agentLifecycle'; import { DEFAULT_FLOW_JUMP_POLICY, @@ -84,6 +86,7 @@ export class AgentFlowService extends Disposable implements IAgentFlowService { @IAgentScopeContext private readonly scopeContext: IAgentScopeContext, @ISkillActivationDataService private readonly activationData: ISkillActivationDataService, @IAgentContextMemoryService private readonly contextMemory: IAgentContextMemoryService, + @IAgentLifecycleService private readonly agentLifecycle: IAgentLifecycleService, @IConfigService config: IConfigService, ) { super(); @@ -197,7 +200,12 @@ export class AgentFlowService extends Disposable implements IAgentFlowService { const flagNow = this.flags.enabled(FLOW_FLAG_ID); if (flagNow === flagWas) return; flagWas = flagNow; - if (!flagNow) this.pendingActivations.clear(); + if (!flagNow) { + this.pendingActivations.clear(); + if (this.scopeContext.agentId === 'main') { + this.agentLifecycle.resolve(agentContextOfScope(this.scopeContext), AgentSkill).abortQueuedFlowPrompts(); + } + } if (this.scopeContext.agentId !== 'main') return; void this.dispatcher.dispatch( new AgentStatusUpdated({ diff --git a/packages/agent-core-v2/test/features/flow/flowService.test.ts b/packages/agent-core-v2/test/features/flow/flowService.test.ts index a0238e1bc..0abaefed1 100644 --- a/packages/agent-core-v2/test/features/flow/flowService.test.ts +++ b/packages/agent-core-v2/test/features/flow/flowService.test.ts @@ -8,6 +8,7 @@ import { ContextAppendMessage, ContextUndo, } from '#/agent/contextMemory/contextEvents'; +import { IAgentLifecycleService } from '#/session/agentLifecycle/agentLifecycle'; import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode'; import { IAgentRuntimeService } from '#/agent/runtimeBinding/agentRuntime'; import { IAgentStateService } from '#/agent/state/agentState'; @@ -88,6 +89,7 @@ describe('AgentFlowService', () => { let activationDataStore: Map; let contextMessages: ContextMessage[]; let configHandlers: ((e: ConfigChangedEvent) => void)[]; + let abortQueuedFlowPrompts: Mock; let flowToolSource: 'builtin' | 'user'; let runtimeText: string | undefined; let hostFsText: string | undefined; @@ -180,6 +182,10 @@ describe('AgentFlowService', () => { setModeAndBroadcast: () => {}, onDidChangeMode: Event.None, } as unknown as IAgentPermissionModeService); + abortQueuedFlowPrompts = vi.fn(); + ix.stub(IAgentLifecycleService, { + resolve: () => ({ abortQueuedFlowPrompts }), + } as unknown as IAgentLifecycleService); registerTestAgentWire(ix, testWireScope('wire', 'flow-test'), { log: ix.get(IAppendLogStore), eventBus: ix.get(IEventBus), @@ -812,6 +818,18 @@ describe('AgentFlowService', () => { sub.dispose(); }); + it('clears pending activations and aborts queued flow prompts when the live flag turns off', async () => { + await activateFlowSkill({ appendPrompt: false, reconcile: false }); + expect(service.hasPendingActivation()).toBe(true); + flowFlagOn = false; + for (const handler of configHandlers) handler({} as ConfigChangedEvent); + expect(service.hasPendingActivation()).toBe(false); + expect(abortQueuedFlowPrompts).toHaveBeenCalledTimes(1); + flowFlagOn = true; + for (const handler of configHandlers) handler({} as ConfigChangedEvent); + expect(abortQueuedFlowPrompts).toHaveBeenCalledTimes(1); + }); + it('vetoes TodoList batched with a FlowStart even before the run is active', async () => { const todoCall: ToolCall = {