Skip to content

fix(mcp): release an idle project on its own deadline (#2087) - #2306

Open
bompus wants to merge 1 commit into
colbymchenry:mainfrom
bompus:fix/idle-release-own-deadline
Open

bompus wants to merge 1 commit into
colbymchenry:mainfrom
bompus:fix/idle-release-own-deadline

Conversation

@bompus

@bompus bompus commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #2283 (#2087).

scheduleIdleRelease returns early when a timer is already armed. The timer is armed for the oldest project a trim can release, and a project whose catch-up is still running is skipped. That leaves one ordering where the deadline is wrong:

  1. Project A is opened, and its catch-up is still running.
  2. Project B is opened later. The trim skips A and arms the timer for B's deadline.
  3. A's catch-up settles. The trim finds A not yet idle and calls scheduleIdleRelease, which keeps B's timer.

A then stays open, writer lock included, until B's deadline, which is up to nearly twice the timeout. With the 10-minute default, a project last used at 0:00 whose catch-up settles at 9:50, while another project was used at 8:20, is released at 18:20 instead of 10:00.

The fix: a trim that gets as far as scheduling now replaces the timer instead of keeping it. Trims skip scheduling while calls are in flight, and the last call to finish trims again, so a settled catch-up still gets its own deadline. There is still one timer and no polling.

Test

__tests__/mcp-idle-release-order.test.ts drives that ordering through execute(). It uses fake timers and a stub ProjectLifecycle that holds A's catch-up until the test releases it. Without the fix nothing has been released at 3.6 s ([]); with it, A is released at 3.5 s and B at 4.5 s.

Changelog

No new entry. #2087 is still under [Unreleased], so no released version has this bug, and its entry already describes the intended behavior.

Checks

  • tsc clean.
  • mcp-idle-release-order and mcp-projectpath-lifecycle pass (24 tests).

)

The idle timer kept whichever deadline was armed first. When an older
project's catch-up was still running, the timer was armed for a newer
project; once the catch-up settled, the older project waited for the
newer deadline, holding its writer lock up to nearly twice the timeout.
Every trim now re-arms the timer for the oldest releasable project.

This branch has not been deployed

No deployments
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.

1 participant