google-gemini / google-gemini/gemini-cli

bug: CoreToolScheduler.dispose() leaves queued schedule() callers pending forever with leaked abort listeners

Open Beginner friendly
#29,030 0 comments 0 reactions 0 assignees View on GitHub
area/agent status/need-triage
Dominant language
TypeScript
Stars
107k
Forks
14.6k
Avg merge
2d 3h
Merged PRs (30d)
45

Description

## What happened?

`CoreToolScheduler.dispose()` removes its MCP-progress subscription and aborts an internal controller, but never drains `requestQueue` nor rejects promises parked in `_enqueueRequest()`. Any caller whose `schedule()` is queued behind an active batch at teardown time has its promise settle **never**, leaking the `'abort'` listener attached to the caller's signal and the queue entry itself.

## Affected code

`packages/core/src/scheduler/scheduler.ts:140-143`:

```ts
dispose(): void {
coreEvents.off(CoreEvent.McpProgress, this.handleMcpProgress);
this.disposeController.abort();
}
```

`packages/core/src/core/../scheduler/scheduler.ts:229-243` — the only paths that settle a queued promise are the caller's own abort signal or batch processing, neither of which can happen after dispose:

```ts
const abortHandler = () => {
const index = this.requestQueue.findIndex((item) => item.requests === requests);
if (index > -1) { this.requestQueue.splice(index, 1); reject(new Error('Tool call cancelled while in queue.')); }
};
// ... resolve/reject wrappers remove the listener only when settled
```

The sole drain point is `_startBatch`'s loop over `requestQueue`; once disposed it never runs again.

## How can this be reproduced?

1. Start a long-running tool batch so subsequent requests queue.
2. While item B sits in `requestQueue`, call `scheduler.dispose()`.
3. The caller of `schedule()` for B awaits forever; inspect `signal.listenerCount('abort')` to see the leaked listener.

## What did you expect to happen?

Dispose should reject every queued promise (e.g., `new Error('Scheduler disposed')`) so callers fail fast and their listeners are removed.

## Impact

Subagent/session teardown leaves callers hung indefinitely and retains closures per queued call.

## Suggested direction

In `dispose()`, splice all queue entries and invoke their stored `reject` (the entries already carry settle callbacks that remove listeners).

---

*Found by source audit on current `main` (commit `5411f113c`); platform-independent. No open issue/PR covering this was found (searched: scheduler dispose).*

Contributor guide

Open the contributing guide

Research direction

Start in packages/core/src/scheduler/scheduler.ts, especially dispose() and the requestQueue handling in _enqueueRequest(). Reproduce a queued schedule() call behind a long-running batch, then dispose the scheduler. Done means every queued promise rejects promptly and the queued callers' abort listeners and queue entries are released.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
cli
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
86/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.