Skip to content

feat(core): limit background agent concurrency#4324

Merged
wenshao merged 2 commits into
QwenLM:mainfrom
kkhomej33-netizen:feat/background-agent-concurrency-limit
May 25, 2026
Merged

feat(core): limit background agent concurrency#4324
wenshao merged 2 commits into
QwenLM:mainfrom
kkhomej33-netizen:feat/background-agent-concurrency-limit

Conversation

@kkhomej33-netizen

Copy link
Copy Markdown
Contributor

Summary

  • What changed:
    • Add a configurable cap for concurrently running background agents.
    • Return a clear tool result when a new background agent cannot start because the cap is reached.
    • Keep the final registry check as the race guard while preflighting before background launch side effects.
  • Why it changed:
    • Background agents run independent reasoning loops. Without a cap, repeated run_in_background launches can consume API quota and local resources without backpressure.
  • Reviewer focus:
    • The cap applies only to running background agents, not foreground agent rows or paused/terminal entries.
    • The agent tool preflight happens before hooks, worktree setup, child agent setup, and transcript creation.

Validation

  • Commands run:
    cd packages/core && npx vitest run src/agents/background-tasks.test.ts src/tools/agent/agent.test.ts
    cd packages/core && npm run typecheck
    cd packages/core && npm run lint
    cd packages/core && npm run build
    git diff --check
  • Prompts / inputs used:
    • Started the bundled CLI with QWEN_CODE_MAX_BACKGROUND_AGENTS=1 node dist/cli.js.
    • Asked the main agent to start one background agent analyzing packages/core/src/tools/agent/agent.ts.
    • Asked it to start another background agent analyzing packages/core/src/agents/background-tasks.ts before the first completed.
  • Expected result:
    • The first background agent starts.
    • The second background agent is rejected with a clear maximum-concurrency message.
    • The already-running background agent remains active.
  • Observed result:
    • The first agent remained active.
    • The second launch returned: Cannot start background agent: maximum concurrent background agents (1) reached. Stop an existing agent first.
  • Quickest reviewer verification path:
    • Run QWEN_CODE_MAX_BACKGROUND_AGENTS=1 node dist/cli.js.
    • Launch two agent tool calls with run_in_background: true before the first completes.
  • Evidence (output, logs, screenshots, video, JSON, before/after, etc.):
    • Manual TUI acceptance screenshot captured the second agent launch failing with the expected message while the first background agent remained active.

Scope / Risk

  • Main risk or tradeoff:
    • The default cap is 10. Workflows that intentionally launch more than 10 concurrent background agents must opt in with QWEN_CODE_MAX_BACKGROUND_AGENTS.
  • Not covered / not validated:
    • Full cross-platform integration suites were not run.
    • Root npm run build is currently blocked in this checkout by existing packages/cli/src/serve/* ACP bridge/status type errors unrelated to this change.
  • Breaking changes / migration notes:
    • No API migration is required. Users can raise the cap with QWEN_CODE_MAX_BACKGROUND_AGENTS=<n>.

Testing Matrix

🍏 🪟 🐧
npm run ⚠️ ⚠️
npx ⚠️ ⚠️
Docker ⚠️ N/A ⚠️
Podman ⚠️ N/A N/A
Seatbelt ⚠️ N/A N/A

Testing matrix notes:

  • macOS package-level npm run build, npm run typecheck, and npm run lint passed in packages/core.
  • macOS npx vitest run src/agents/background-tasks.test.ts src/tools/agent/agent.test.ts passed in packages/core.
  • Docker, Podman, Seatbelt, Windows, and Linux were not run for this focused registry/tool change.

Linked Issues / Bugs

@kkhomej33-netizen

kkhomej33-netizen commented May 19, 2026

Copy link
Copy Markdown
Contributor Author

E2E Test Report

Manual TUI acceptance was run with:

QWEN_CODE_MAX_BACKGROUND_AGENTS=1 node dist/cli.js

Scenario:

  1. Start one background agent analyzing packages/core/src/tools/agent/agent.ts.
  2. Before it completes, ask the main agent to start another background agent analyzing packages/core/src/agents/background-tasks.ts.

Expected:

  • The first background agent remains active.
  • The second launch is rejected with the concurrency-limit message.

Observed:

  • The footer showed one active local agent.
  • The second launch returned:
Cannot start background agent: maximum concurrent background agents (1) reached. Stop an existing agent first.

This matches the expected behavior for the new background-agent concurrency cap.

用户附件

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical regression in resume path: The new cap check in register() also fires on the resume path in background-agent-resume.ts (unmodified by this PR). That file calls register() twice for the same agentId — first at line 490 (promoting pausedrunning, outside any try/catch) and again at line 658. The second call's cap check counts the just-promoted entry toward the cap, so the last concurrency slot can never be filled by a resume operation. The first call's throw (when the cap is already full) becomes an unhandled rejection. Fix: skip the cap check in register() when replacing an existing entry with the same agentId that is already running + isBackgrounded.


register(registration: AgentTaskRegistration): AgentTask {
if (registration.isBackgrounded && registration.status === 'running') {
this.assertCanStartBackgroundAgent();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] The cap check added here applies to ALL callers of register(), including the resume path in background-agent-resume.ts. That file calls register() twice for the same agentId (line 490 outside try/catch, line 658 inside try/catch). The first call promotes pausedrunning, incrementing the cap count. The second call then sees count >= cap and throws — even though it's replacing the same slot, not adding a new agent. Net effect: the last concurrency slot can never be filled by a resume operation, and the first call's throw becomes an unhandled rejection.

Suggested change
this.assertCanStartBackgroundAgent();
register(registration: AgentTaskRegistration): AgentTask {
if (registration.isBackgrounded && registration.status === 'running') {
const existing = this.agents.get(registration.agentId);
const isReplacingRunning = existing?.isBackgrounded && existing?.status === 'running';
if (!isReplacingRunning) {
this.assertCanStartBackgroundAgent();
}
}

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

outputFile: jsonlPath,
metaPath,
});
} catch (error) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] The SubagentStart hook fires at line 1622 before this register() call. If register() throws (cap reached in the TOCTOU window), this catch block does NOT fire a matching SubagentStop hook. Hook consumers (metrics, resource allocators, billing) see orphaned "started" events with no corresponding "stopped."

Add a SubagentStop fire in this catch block:

// After bgAbortController.abort():
const hookSystem = this.config.getHookSystem();
try {
  await this.runSubagentStopHookLoop(
    hookOpts.agentId, hookOpts.agentType,
    jsonlPath, undefined, false, resolvedMode, signal, updateOutput,
  );
} catch (hookError) {
  debugLogger.warn(`[Agent] SubagentStop hook after register failure: ${hookError}`);
}

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

export const BACKGROUND_AGENT_CONCURRENCY_ENV =
'QWEN_CODE_MAX_BACKGROUND_AGENTS';

export function resolveMaxConcurrentBackgroundAgents(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Invalid env var values (NaN, negative, float, non-numeric strings) silently fall back to the default (10) with no log output. An operator who sets QWEN_CODE_MAX_BACKGROUND_AGENTS=3.5 or =foo gets no indication that their value was ignored.

Suggested change
export function resolveMaxConcurrentBackgroundAgents(
export function resolveMaxConcurrentBackgroundAgents(
env: Record<string, string | undefined> = process.env,
): number {
const raw = env[BACKGROUND_AGENT_CONCURRENCY_ENV];
if (raw === undefined || raw.trim() === '') {
return DEFAULT_MAX_CONCURRENT_BACKGROUND_AGENTS;
}
const parsed = Number(raw);
if (!Number.isInteger(parsed) || parsed < 1) {
debugLogger.warn(
`[BackgroundTasks] Invalid ${BACKGROUND_AGENT_CONCURRENCY_ENV}=${JSON.stringify(raw)}, using default (${DEFAULT_MAX_CONCURRENT_BACKGROUND_AGENTS})`,
);
return DEFAULT_MAX_CONCURRENT_BACKGROUND_AGENTS;
}
return parsed;
}

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

: MAX_CONCURRENT_BACKGROUND_AGENTS;
}

assertCanStartBackgroundAgent(): void {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] No debugLogger call when the cap is hit. Operators investigating why background agents aren't starting have no log evidence — the only signal is in the model's conversation context.

Suggested change
assertCanStartBackgroundAgent(): void {
assertCanStartBackgroundAgent(): void {
const running = this.getRunningBackgroundCount();
if (running >= this.maxConcurrentBackgroundAgents) {
debugLogger.warn(
`[BackgroundTasks] Concurrency cap reached: ${running}/${this.maxConcurrentBackgroundAgents}. Refusing new background agent.`,
);
throw new Error(

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

this.params.run_in_background === true ||
subagentConfig.background === true;

if (shouldRunInBackground) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The preflight check here and the authoritative check inside register() serve different purposes but neither documents the relationship. A future maintainer may remove one as "redundant." Add a comment:

Suggested change
if (shouldRunInBackground) {
// Preflight: fast-fail before expensive worktree/subagent setup.
// NOT redundant with the check inside registry.register() — that
// one is the authoritative guard, but by then we've already
// provisioned a worktree and created the subagent.
if (shouldRunInBackground) {

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

},
updateOutput,
);
void agentConfig

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] agentConfig.getToolRegistry().stop() uses fire-and-forget .catch(() => {}). If it rejects, tool processes (spawned shells, MCP connections) may outlive the failed agent with no log evidence. Replace with:

Suggested change
void agentConfig
void agentConfig
.getToolRegistry()
.stop()
.catch((e) => {
debugLogger.warn(`[Agent] ToolRegistry stop after register failure: ${e}`);
});

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

@kkhomej33-netizen

Copy link
Copy Markdown
Contributor Author

Thanks for the careful review. I pushed 0f8a8d43f with fixes for the requested changes.

Changes made:

  • Fixed the resume regression:
    • BackgroundTaskRegistry.register() now skips the cap check when replacing the same already-running background agent id.
    • BackgroundAgentResumeService now handles a full-cap resume attempt gracefully: the task stays paused, metadata records the error, and the resume returns undefined instead of leaking a rejected promise.
  • Covered the resume paths with tests:
    • resuming into the final available slot
    • attempting to resume while all slots are already occupied
  • Covered the TOCTOU launch path:
    • if the final register() guard fails after SubagentStart, the agent tool now emits a matching SubagentStop hook and still avoids launching the background body.
  • Added the requested observability/maintainability polish:
    • warn on invalid QWEN_CODE_MAX_BACKGROUND_AGENTS
    • warn when the background cap is hit
    • document why the preflight and final register() check both exist
    • log ToolRegistry.stop() failures after registration failure

Validation rerun:

cd packages/core && npx vitest run src/agents/background-tasks.test.ts src/agents/background-agent-resume.test.ts src/tools/agent/agent.test.ts
cd packages/core && npm run typecheck
cd packages/core && npm run lint
cd packages/core && npm run build
git diff --check

All passed locally.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 2 review: all round 1 findings addressed. The isReplacingRunning escape hatch correctly fixes the resume-path cap self-collision, and the subagentStartHookCompleted flag properly guards hook symmetry. New tests cover the key paths (resume at cap, resume when full, register error handling, stop hook after register failure). 164 tests pass, tsc + eslint clean. LGTM! ✅ — qwen-latest-series-invite-beta-v34 via Qwen Code /review

@pomelo-nwu pomelo-nwu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Add configurable concurrency cap (default 10) for background agents to prevent resource exhaustion, with clear rejection message when limit is reached.

@wenshao

wenshao commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification report — PR 4324

Local merge-prep verification by @wenshao on a worktree of this PR merged with
current main. All checks pass. Recommended to merge.

Scope reminder

Two commits, six files, all in packages/core:

  • 860b40bf1 feat(core): limit background agent concurrency
  • 0f8a8d43f fix(core): handle background agent cap on resume

Adds a configurable cap on running, backgrounded agents
(QWEN_CODE_MAX_BACKGROUND_AGENTS, default 10, invalid values warn and
fall back to default). Enforcement points:

  1. BackgroundTaskRegistry.assertCanStartBackgroundAgent() — new method.
  2. BackgroundTaskRegistry.register() — authoritative race guard. Foreground,
    paused, terminal, and same-id replacement do not count.
  3. AgentToolInvocation.execute() — preflight before hooks / worktree /
    subagent creation, so a doomed launch is rejected fast.
  4. BackgroundAgentResumeService.resumeBackgroundAgent() — try/catch around
    the resume re-register; on rejection restores the paused entry and writes
    lastError to the meta sidecar.

Environment

  • Linux x86_64, Node v22.22.2.
  • PR fetched into a worktree, then git merge main --no-editclean, no
    conflicts
    . After merge, package-lock.json is byte-identical to main.

Results

Check Result
vitest run on the 3 targeted suites ✅ 164 tests / 3 files pass (5.50s)
npm run typecheck (packages/core) ✅ clean
npm run lint (packages/core) ✅ clean
npm run build ✅ clean (15 pre-existing curly warnings in vscode-ide-companion, unrelated)
npm run bundle dist/cli.js 6.07 MB; new symbols present in chunk
Direct smoke test against compiled output ✅ see below
Real TUI test in tmux with QWEN_CODE_MAX_BACKGROUND_AGENTS=1 ✅ see below

Compiled-output smoke test

A small Node script imports the compiled registry from
packages/core/dist/src/agents/background-tasks.js and exercises the runtime
path:

PASS env name constant: "QWEN_CODE_MAX_BACKGROUND_AGENTS"
PASS default cap: 10
PASS module cap: 10
PASS resolver default: 10
PASS resolver override valid: 3
PASS resolver invalid (0) falls back: 10
PASS first background register succeeds: "bg-1"
PASS second background register at cap throws expected:
     "Cannot start background agent: maximum concurrent background agents (1) reached. Stop an existing agent first."
PASS foreground entry does not count toward cap: "bg-2"
PASS paused entry does not count, can re-register paused with same id: "running"
SMOKE OK

Real TUI test in tmux

Launched the bundled CLI under QWEN_CODE_MAX_BACKGROUND_AGENTS=1. Sent a
single user message asking the model to fire two agent tool calls in
parallel, both with run_in_background: true.

Captured pane (excerpt):

╭────────────────────────────────────────────────────────────────────╮
│ ✓  Agent Probe A                                                   │
│                                                                    │
│ ✓  Agent Probe B                                                   │
│                                                                    │
│     ✖ Explore: Probe B · Cannot start background agent:            │
│       maximum concurrent background agents (1) reached.            │
│       Stop an existing agent first.                                │
╰────────────────────────────────────────────────────────────────────╯

Active agents (1/1)
○ Explore: Probe A ▶ 18s

Matches the PR description verbatim:

  • Probe A (first) admitted and remained running.
  • Probe B (second) rejected with the exact expected message.
  • Active-agents counter shows 1/1, reflecting the cap.
  • Rejection is a per-call failure inside the tool group — the parent turn was
    not aborted.

When Probe A completed (after 2m 11s · 20k tokens), the counter dropped to
0/1 and the agent appeared as completed, confirming slot release:

... 1 task done
Active agents (0/1)
✔ Explore: Probe A (ReadFile sample.txt) ▶ 2m 11s · 20k tokens

Things to be aware of as the maintainer

  • Default is 10, not unlimited. Workflows that intentionally fan out more
    than ten concurrent background agents must set
    QWEN_CODE_MAX_BACKGROUND_AGENTS=<n>.
  • Rejected resume: if a paused background agent cannot reclaim a slot, it
    stays paused with error populated. The user can retry once another agent
    stops. No automatic retry/queue — documented in the new tests.
  • packages/cli is not touched. The existing Active agents (X/N)
    counter already displays the new cap correctly; no UI snapshot updates
    needed.
  • The PR is two commits. Logically tight (the second commit closes a
    missing case in resume) and can land as-is.

Reproduction

git fetch origin pull/4324/head:pr-4324
git worktree add /tmp/pr-4324 pr-4324
cd /tmp/pr-4324
git merge main --no-edit         # clean
npm ci

cd packages/core
npx vitest run \
  src/agents/background-tasks.test.ts \
  src/tools/agent/agent.test.ts \
  src/agents/background-agent-resume.test.ts
npm run typecheck && npm run lint

cd /tmp/pr-4324
npm run build && npm run bundle

QWEN_CODE_MAX_BACKGROUND_AGENTS=1 \
  node /tmp/pr-4324/dist/cli.js --yolo
# Then prompt the model to call the `agent` tool twice with run_in_background: true.

Verdict

All behavioral claims in the PR description reproduced on a tree merged with
current main. The change is small, focused, well-tested (164 tests including
resume edge cases), and the user-facing failure mode is clear and actionable.
LGTM to merge.

@wenshao wenshao merged commit 632865c into QwenLM:main May 25, 2026
10 checks passed
tanzhenxin added a commit that referenced this pull request Jun 1, 2026
…ask module

Folds PR #4324's per-instance BackgroundTaskRegistry cap into the
collapsed tasks/agent-task.ts module as kind-local module state.
The new architecture's per-kind module is the natural home for an
agent-only behavior — the generic TaskRegistry no longer needs
to carry it.

What moves into tasks/agent-task.ts:

  - MAX_CONCURRENT_BACKGROUND_AGENTS and the env-var resolver
  - agentAssertCanStartBackground(registry) free function
  - The race-guard inside agentRegister() (skips re-registers of
    already-running entries so resume can recover)
  - setAgentBackgroundCapForTest() for test override (mirrors the
    existing setAgentNotificationCallback pattern)

What moves out of agent.ts:

  - The early preflight calls agentAssertCanStartBackground instead
    of registry.assertCanStartBackgroundAgent().
  - The post-register error path is unchanged structurally — it still
    aborts the bgAbortController, fires SubagentStop, cleans up the
    worktree, and returns the cap message to the model.

Resume path (background-agent-resume.ts) now wraps agentRegister
in the same try/catch #4324 added, but routes to the new free
function.

Also addresses wenshao's review on #3982:

  1. config.ts — moves the four registerTaskKind(...) calls below
     all imports, instead of dangling between two import groups.
  2. dispatcher.ts — comment updated to describe actual init order
     (module-load side effect in config.ts), removes the
     fictional registerAllTaskKinds() / Config.initialize()
     references.
  3. monitor.ts — fixes a stale comment that still referred to
     monitorRegister(registry, ) (dangling comma).
  4. monitor-task.ts — monitorAbortAll now clears the module-level
     owner-routed callbacks so a daemon process recycling sessions
     doesn't leak handlers to dead owner agents.
  5. useBackgroundTaskView.ts — moves the registry shape probe to
     getAll() BEFORE buildMerged() so activity bursts and
     monitor event bumps don't pay the listDreamTasks cost.

Tests added:

  - tasks/agent-task.test.ts — covers env resolution, cap
    enforcement (running-only, foreground exclusion, paused
    exclusion, resume race exception), and module-level cap reset.

Tests fixed (rebase fallout):

  - nonInteractiveCli.test.ts — six structured-output assertions
    migrated from mockBackgroundTaskRegistry.abortAll to the
    module-level mockAgentAbortAll mock.
  - shell.test.ts — 13 assertions migrated from
    registry.{cancel,complete,fail} to
    shellTaskModule.shell{Cancel,Complete,Fail} (called with
    (registry, ...)).
  - useBackgroundTaskView.test.ts — sort assertions updated to the
    new descending-startTime active-bucket order.
  - agent.test.ts — adds the getStopHookBlockingCap mock that
    was lost in the auto-merge with PR #4208.
  - clearCommand.test.ts — drops the SessionStart assertion that
    was lost in the auto-merge with PR #4115.
  - background-agent-resume.test.ts — monitorRegistry is now a
    set of vi.spyOn against the new module-level free functions.
  - InlineParallelAgentsDisplay.test.tsx — makeRegistryConfig
    exposes getTaskRegistry (matching the renamed Config method).
tanzhenxin added a commit that referenced this pull request Jun 8, 2026
…ask module

Folds PR #4324's per-instance BackgroundTaskRegistry cap into the
collapsed tasks/agent-task.ts module as kind-local module state.
The new architecture's per-kind module is the natural home for an
agent-only behavior — the generic TaskRegistry no longer needs
to carry it.

What moves into tasks/agent-task.ts:

  - MAX_CONCURRENT_BACKGROUND_AGENTS and the env-var resolver
  - agentAssertCanStartBackground(registry) free function
  - The race-guard inside agentRegister() (skips re-registers of
    already-running entries so resume can recover)
  - setAgentBackgroundCapForTest() for test override (mirrors the
    existing setAgentNotificationCallback pattern)

What moves out of agent.ts:

  - The early preflight calls agentAssertCanStartBackground instead
    of registry.assertCanStartBackgroundAgent().
  - The post-register error path is unchanged structurally — it still
    aborts the bgAbortController, fires SubagentStop, cleans up the
    worktree, and returns the cap message to the model.

Resume path (background-agent-resume.ts) now wraps agentRegister
in the same try/catch #4324 added, but routes to the new free
function.

Also addresses wenshao's review on #3982:

  1. config.ts — moves the four registerTaskKind(...) calls below
     all imports, instead of dangling between two import groups.
  2. dispatcher.ts — comment updated to describe actual init order
     (module-load side effect in config.ts), removes the
     fictional registerAllTaskKinds() / Config.initialize()
     references.
  3. monitor.ts — fixes a stale comment that still referred to
     monitorRegister(registry, ) (dangling comma).
  4. monitor-task.ts — monitorAbortAll now clears the module-level
     owner-routed callbacks so a daemon process recycling sessions
     doesn't leak handlers to dead owner agents.
  5. useBackgroundTaskView.ts — moves the registry shape probe to
     getAll() BEFORE buildMerged() so activity bursts and
     monitor event bumps don't pay the listDreamTasks cost.

Tests added:

  - tasks/agent-task.test.ts — covers env resolution, cap
    enforcement (running-only, foreground exclusion, paused
    exclusion, resume race exception), and module-level cap reset.

Tests fixed (rebase fallout):

  - nonInteractiveCli.test.ts — six structured-output assertions
    migrated from mockBackgroundTaskRegistry.abortAll to the
    module-level mockAgentAbortAll mock.
  - shell.test.ts — 13 assertions migrated from
    registry.{cancel,complete,fail} to
    shellTaskModule.shell{Cancel,Complete,Fail} (called with
    (registry, ...)).
  - useBackgroundTaskView.test.ts — sort assertions updated to the
    new descending-startTime active-bucket order.
  - agent.test.ts — adds the getStopHookBlockingCap mock that
    was lost in the auto-merge with PR #4208.
  - clearCommand.test.ts — drops the SessionStart assertion that
    was lost in the auto-merge with PR #4115.
  - background-agent-resume.test.ts — monitorRegistry is now a
    set of vi.spyOn against the new module-level free functions.
  - InlineParallelAgentsDisplay.test.tsx — makeRegistryConfig
    exposes getTaskRegistry (matching the renamed Config method).
tanzhenxin added a commit that referenced this pull request Jun 8, 2026
…ask module

Folds PR #4324's per-instance BackgroundTaskRegistry cap into the
collapsed tasks/agent-task.ts module as kind-local module state.
The new architecture's per-kind module is the natural home for an
agent-only behavior — the generic TaskRegistry no longer needs
to carry it.

What moves into tasks/agent-task.ts:

  - MAX_CONCURRENT_BACKGROUND_AGENTS and the env-var resolver
  - agentAssertCanStartBackground(registry) free function
  - The race-guard inside agentRegister() (skips re-registers of
    already-running entries so resume can recover)
  - setAgentBackgroundCapForTest() for test override (mirrors the
    existing setAgentNotificationCallback pattern)

What moves out of agent.ts:

  - The early preflight calls agentAssertCanStartBackground instead
    of registry.assertCanStartBackgroundAgent().
  - The post-register error path is unchanged structurally — it still
    aborts the bgAbortController, fires SubagentStop, cleans up the
    worktree, and returns the cap message to the model.

Resume path (background-agent-resume.ts) now wraps agentRegister
in the same try/catch #4324 added, but routes to the new free
function.

Also addresses wenshao's review on #3982:

  1. config.ts — moves the four registerTaskKind(...) calls below
     all imports, instead of dangling between two import groups.
  2. dispatcher.ts — comment updated to describe actual init order
     (module-load side effect in config.ts), removes the
     fictional registerAllTaskKinds() / Config.initialize()
     references.
  3. monitor.ts — fixes a stale comment that still referred to
     monitorRegister(registry, ) (dangling comma).
  4. monitor-task.ts — monitorAbortAll now clears the module-level
     owner-routed callbacks so a daemon process recycling sessions
     doesn't leak handlers to dead owner agents.
  5. useBackgroundTaskView.ts — moves the registry shape probe to
     getAll() BEFORE buildMerged() so activity bursts and
     monitor event bumps don't pay the listDreamTasks cost.

Tests added:

  - tasks/agent-task.test.ts — covers env resolution, cap
    enforcement (running-only, foreground exclusion, paused
    exclusion, resume race exception), and module-level cap reset.

Tests fixed (rebase fallout):

  - nonInteractiveCli.test.ts — six structured-output assertions
    migrated from mockBackgroundTaskRegistry.abortAll to the
    module-level mockAgentAbortAll mock.
  - shell.test.ts — 13 assertions migrated from
    registry.{cancel,complete,fail} to
    shellTaskModule.shell{Cancel,Complete,Fail} (called with
    (registry, ...)).
  - useBackgroundTaskView.test.ts — sort assertions updated to the
    new descending-startTime active-bucket order.
  - agent.test.ts — adds the getStopHookBlockingCap mock that
    was lost in the auto-merge with PR #4208.
  - clearCommand.test.ts — drops the SessionStart assertion that
    was lost in the auto-merge with PR #4115.
  - background-agent-resume.test.ts — monitorRegistry is now a
    set of vi.spyOn against the new module-level free functions.
  - InlineParallelAgentsDisplay.test.tsx — makeRegistryConfig
    exposes getTaskRegistry (matching the renamed Config method).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants