fix(flow): abort queued flow prompts when the flow flag turns off

This commit is contained in:
Kaiyi 2026-08-24 22:18:46 +08:00
parent 4b1a789189
commit 8956084e77
2 changed files with 28 additions and 2 deletions

View file

@ -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({

View file

@ -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<string, unknown>;
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 = {