fix(core): enforce agent concurrency cap on foreground sub-agents (#6290) (#6300)

The concurrency cap (QWEN_CODE_MAX_BACKGROUND_AGENTS) only checked
backgrounded agents, letting foreground agents like Explorer bypass
it entirely. Count all running agents toward the cap and apply the
preflight check regardless of background/foreground flavor.

Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
Kagura 2026-07-04 22:16:33 +08:00 committed by GitHub
parent aa4bccfc47
commit 4400fe6a38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 28 additions and 26 deletions

View file

@ -511,16 +511,24 @@ describe('BackgroundTaskRegistry', () => {
expect(registry.get('bg-1')?.prompt).toBe('resumed continuation');
});
it('does not count foreground, paused, or terminal entries toward the cap', () => {
it('counts foreground agents toward the cap but not paused or terminal entries', () => {
registry = new BackgroundTaskRegistry({
maxConcurrentBackgroundAgents: 1,
});
// A foreground agent occupies a slot.
registry.register(
makeRegistration('fg-1', {
isBackgrounded: false,
}),
);
expect(() => registry.register(makeRegistration('bg-1'))).toThrow(
'maximum concurrent background agents (1) reached',
);
// Paused entries do not occupy a slot.
registry.unregisterForeground('fg-1');
registry.register(
makeRegistration('paused-1', {
status: 'paused',
@ -535,7 +543,6 @@ describe('BackgroundTaskRegistry', () => {
registry.complete('bg-1', 'done');
registry.register(makeRegistration('bg-2'));
expect(registry.get('fg-1')).toBeDefined();
expect(registry.get('paused-1')).toBeDefined();
expect(registry.get('bg-2')?.status).toBe('running');
});

View file

@ -428,10 +428,9 @@ export class BackgroundTaskRegistry {
registration: AgentTaskRegistration,
options: BackgroundTaskRegisterOptions = {},
): AgentTask {
if (registration.isBackgrounded && registration.status === 'running') {
if (registration.status === 'running') {
const existing = this.agents.get(registration.agentId);
const isReplacingRunning =
existing?.isBackgrounded === true && existing.status === 'running';
const isReplacingRunning = existing?.status === 'running';
if (!isReplacingRunning) {
this.assertCanStartBackgroundAgent();
}
@ -852,7 +851,7 @@ export class BackgroundTaskRegistry {
private getRunningBackgroundCount(): number {
return Array.from(this.agents.values()).filter(
(entry) => entry.isBackgrounded && entry.status === 'running',
(entry) => entry.status === 'running',
).length;
}

View file

@ -2135,26 +2135,22 @@ class AgentToolInvocation extends BaseToolInvocation<AgentParams, ToolResult> {
// This is not redundant with registry.register() below — that call
// remains the authoritative race guard, but by then the launch path
// has already run hooks and created a child agent.
if (shouldRunInBackground) {
try {
this.config
.getBackgroundTaskRegistry()
.assertCanStartBackgroundAgent();
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
this.updateDisplay(
{
status: 'failed',
terminateReason: errorMessage,
},
updateOutput,
);
return {
llmContent: errorMessage,
returnDisplay: this.currentDisplay!,
};
}
try {
this.config.getBackgroundTaskRegistry().assertCanStartBackgroundAgent();
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
this.updateDisplay(
{
status: 'failed',
terminateReason: errorMessage,
},
updateOutput,
);
return {
llmContent: errorMessage,
returnDisplay: this.currentDisplay!,
};
}
// ── Optional worktree isolation (Phase 1: provision) ──────────