Repository navigation
fix: an idle session's backend and its MCP servers are stopped, and resume on the next message - #452
Merged
Merged
Conversation
…d, and resume on the next message (#451) Every session that had run a turn kept its backend process (the Claude CLI, codex, pi, the Gemini CLI, Qwen) and the MCP servers it started for the daemon's lifetime, so memory grew with the number of sessions ever used. On a 32 GB Mac thirteen of them, most idle for 12 hours to a day and a half, filled RAM and swap. - `session.idleReleaseMinutes` (default 30; `CODEOID_IDLE_RELEASE_MINUTES`, and in Settings): a session idle that long has its backend released through the existing `releaseIdleProvider` path, and the next message starts it again on the same conversation. 0 keeps the old behaviour. - The countdown starts each time the session goes idle (or its background work drains while idle) and is cleared by any activity or a destroy. It never releases a session with a turn running, background work, a pending approval, or a backend that never started; HTTP backends hold no process and are left alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…what the backend left running (#451) - A session whose last turn failed (status error) is at rest too, and is released; so is a pending account-change release. - A limit past setTimeout's 2^31-1 ms is waited out in steps instead of firing after 1 ms. - The backend's own (ambient) watchers don't hold it. - An idle release that finds the session busy is skipped, not turned into a release the moment that work ends. - Both releases snapshot the backend's process tree before the teardown and kill what survives it, as terminateBackend does: codex and the Gemini CLI are SIGKILLed and used to leave their MCP servers and commands running. - docs: an idle release re-reads the project's .mcp.json and hooks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rsharath
approved these changes
Nov 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #451.
Summary
A session left idle for 30 minutes (configurable) now has its backend process, and everything that process started (MCP servers, commands still running), stopped. The next message starts it again and resumes the same conversation. Before, every session that had run a turn kept them for the daemon's whole lifetime.
What was wrong
codeoid only ever stopped a session's backend when its account changed. So the Claude CLI (or codex, pi, the Gemini CLI, Qwen) and every MCP server it launched (Jira, Confluence, Bitbucket, Teams, memory, …) stayed running for every session ever used. Memory grew with the number of sessions touched, not the number working.
Why it matters
On a 32 GB Mac, thirteen Claude processes, most idle for 12 hours to a day and a half, plus their MCP servers filled RAM and swap (58.7 of 59.4 GB) until macOS reported "out of application memory". The same pattern shows on Linux, at about 500 MB per idle session.
What changed
session.idleReleaseMinutes: default 30,0keeps the old behaviour. AlsoCODEOID_IDLE_RELEASE_MINUTES, and in the Settings tab.terminateBackendalready does. codex and the Gemini CLI are stopped with a bare SIGKILL, so without this their MCP servers and commands would outlive them. An account-change release now reaps the same way..mcp.jsonand hooks (documented in docs/mcp-backends.md). An agent that has kept a dev server running in the background past the limit, without the backend reporting it, has that server stopped.Verification
0keeps it; a just-idle session keeps it; HTTP backends untouched; live background work keeps it; a destroyed session's countdown never fires; a failed turn is released; a 100,000-minute limit doesn't fire; an ambient watcher doesn't hold it; a real child process left by the backend is killed.🤖 Implementation context (for agents / maintainers)
Files
src/daemon/session.ts:#idleReleaseTimer,#armIdleRelease()/#clearIdleRelease()besidereleaseIdleProvider(); armed in#setStatusonidleorerror(AT_REST; cleared on any other status), re-armed in#reconcileDrainedBackgroundand when only ambient tasks remain, cleared indestroy(). The countdown is chained inMAX_TIMER_MSsteps to a deadline. The callback re-checks the status,!#turnActive(),#hasAgentBackgroundWork()(ignoresambient), no#pendingApprovals, andprovider.hasQueried, then callsreleaseIdleProvider({ idle: true }), which skips (rather than defers) when busy. Both release paths reap withsnapshotTree/killSurvivors, audited assession.release_backend.reaped. OnlyPROCESS_BACKENDS. The timer isunref'd.src/config.ts:SessionSchema.idleReleaseMinutes(z.number().min(0).default(30)), the session type, and theCODEOID_IDLE_RELEASE_MINUTESenv mapping (float).src/daemon/settings/manifest.ts: the Settings entry.src/tests/idle-release.test.ts; docs/CONFIGURATION.md; CHANGELOG.Follow-ups
🤖 Generated with Claude Code