You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Idle language servers stay running while their project root is in use: idle reap is per root, not per server (31.5 GiB of tsserver in swap) #1170
AFT stops language servers only when a whole project root is idle. A root that has continuous use keeps every server that it started, also servers for package directories that nobody has used for hours. TypeScript servers use approximately 1 GiB each, so the memory increases with each new package directory, and the operating system moves them to swap.
What I measured (aft 0.62.0, one module process, Linux)
34 typescript-language-server processes (68 tsserver.js processes), all children of the aft process. Together they used 31.5 GiB of swap and 7 GiB of resident memory. The aft process itself used 0.9 GiB resident and 2.5 GiB swap.
Each server has a different package directory (the nearest tsconfig.json). Most of the directories are git worktrees in a gitignored directory below the project root. Agents in long sessions in that root create a new worktree for each task.
Server ages were 0.2 h to 10.7 h. For one server, the last request that AFT sent to it was 8 hours before the measurement.
In 12.5 h, the module log has one idle lsp reap line (a different root). lsp.idle_minutes is not set, so the default (60) applies.
I stopped one server with SIGTERM. AFT logged exited TypeScript … (eof) and started a new server on the next request. The diagnostics were correct. Thus a stopped server has no bad effect, but AFT does not stop idle servers itself.
Cause (v0.62.0)
reap_idle_lsp_servers (crates/aft/src/subc/mod.rs:2607) calls shutdown_idle_lsp_at with the root's last_touched. shutdown_idle_lsp_at (crates/aft/src/runtime_drain.rs:4051) compares the idle time of the root with the TTL. Then it stops all clients with take_all_clients(), or it stops none. last_touched changes on each tool call for the root. So while one session in the root makes calls, no server of that root stops. This is also true for a server that got no request for hours. There is no limit on the number of servers for each root.
The standalone path (shutdown_idle_lsp, which uses last_request_at()) has the same all-or-nothing behavior.
Suggested change
Record the last use for each server (a request sent to it, or a document open, change or save for a file that it serves).
In the sweep, stop each server whose last use is older than lsp.idle_minutes, also when the root is in use. Use the current background stop (spawn_idle_lsp_reap).
Possibly: a limit on the number of servers for each root, which stops the server with the oldest last use when a new server starts.
Other observation
A memory check that reads only the aft process (resident memory and swap) does not include the language server children. So that check does not see this increase.
Thanks, I see 7f9d0c0 on main (per-client idle reap). I corrected the line references in the description: they now point to v0.62.0, where I measured the problem. Before, they pointed to an old commit.
Thanks for the detailed measurements and source investigation. Your diagnosis is correct: the idle sweep used the project's activity timestamp and kept all its language servers alive while any tool call kept the project busy. The standalone sweep had the same all-or-nothing behavior.
The fix tracks each server's outgoing requests and document open/change/save notifications, and stops idle servers in the background after lsp.idle_minutes even when another package in the project is busy. In-flight requests are protected, and the next request to a reaped package starts a new server. This is fixed in 0.63.4, released today.
Activity
iceteaSA commentedon Oct 30, 2026
Thanks, I see 7f9d0c0 on main (per-client idle reap). I corrected the line references in the description: they now point to v0.62.0, where I measured the problem. Before, they pointed to an old commit.
0.63.4 notes: per-server idle language-server stop (#1170), runon wai…
Merge origin/main into land/inspect-current-content; pin the approved…
Keep full-document didChange counting as use; restore the unordered s…
aft-alfonso commentedon Oct 30, 2026
Thanks for the detailed measurements and source investigation. Your diagnosis is correct: the idle sweep used the project's activity timestamp and kept all its language servers alive while any tool call kept the project busy. The standalone sweep had the same all-or-nothing behavior.
The fix tracks each server's outgoing requests and document open/change/save notifications, and stops idle servers in the background after
lsp.idle_minuteseven when another package in the project is busy. In-flight requests are protected, and the next request to a reaped package starts a new server. This is fixed in 0.63.4, released today.aft-alfonso commentedon Oct 30, 2026
Released in v0.63.4.
mason: reap idle language servers per client (cortexkit#1170)
mason: bound idle language server restart attempts (cortexkit#1170)
Search descriptor for per-server idle language-server stop (cortexkit…
Merge pull request cortexkit#1179 from cortexkit/land/lsp-idle-per-se…