Relaymote 如何先以 dry-run 安全清理閒置 CLI,釋放每個約 400 MB 記憶體
Freeing 400 MB idle processes without breaking anything: ship the cleanup in dry-run first
Relaymote 的可選 idle-CLI reaper 透過應用程式自身的結束流程清理閒置 CLI,避免外部強制終止令工作階段消失。每個閒置工作階段約佔 400 MB;閒置門檻設為 3 小時,因提示詞快取一小時後已過期。功能預設關閉,啟用後先以 dry mode 記錄 24 小時,之後自動轉為 live;每個週期最多釋放六個工作階段。
Disclosure: I'm the developer of the tool described here, and this post was drafted and published by an AI agent from the project's source code. Relaymote is unofficial; not affiliated with or endorsed by Anthropic.
Relaymote (MIT, Node.js) is a phone remote for Claude Desktop's Code-tab sessions. People who use it tend to keep a lot of sessions open, and every open session keeps a CLI process alive, with its tool servers, at roughly 400 MB each, whether anyone is talking to it or not. Twenty open sessions is a lot of memory doing nothing.
The fix sounds trivial: find the idle ones and stop them. It is the kind of cleanup that is easy to write and easy to get wrong, because the cost of a false positive is not "some memory stayed used", it is "a session the user was relying on vanished". This post is how the optional "idle-CLI reaper" (lib/reaper.js, about 200 lines) is built so that it can only ever do the boring, safe thing.
1. Don't kill the process. Use the app's own exit path.
The obvious implementation is taskkill /PID. Don't. From the host application's point of view, an external kill is a process loss: something went wrong, report it, maybe try to recover. That is a different code path from a session that simply finished.
The app already has a teardown routine it uses itself when a query ends. Calling that instead leaves the session exactly as a freshly launched app would: the record and transcript intact, no process. The next message the user sends respawns the CLI and resumes from the transcript, the same as after an app restart.
The general rule: when you clean up somebody else's resources, find the path they use to clean up their own, and take that one.
2. Make "releasing costs nothing" a measured claim
Why three hours? Because after an hour the session's prompt cache has expired anyway, so the next message pays the cold-start cost whether the process was kept or not. Releasing a session that has been idle for three hours therefore costs nothing in tokens. The threshold is not a guess about user behaviour; it sits past the point where keeping the process stopped buying anything.
3. Write the guards as a list of reasons, not one boolean
The check runs inside the desktop app at the moment of release, and for every session it builds an array of reasons to spare it:
var why = [];
if (s.isRunning) why.push('running');
try { if (m.hasPendingUserInput(s)) why.push('awaiting-input'); } catch (e) { why.push('input-unknown'); }
try { if (!m.isCliProvablyIdle(s)) why.push('cli-not-provably-idle'); } catch (e) { why.push('idle-unknown'); }
if (idleH < o.idleHours) why.push('idle<' + o.idleHours + 'h');
if ((s.lastActivityAt || 0) > (s.lastFocusedAt || 0) + 60e3) why.push('unread');
if (o.protect.indexOf(id) >= 0) why.push('goal-or-await');
// ...queued input, background tasks or crons, scheduled-task sessions, SSH/WSL, archived, stopping
A session is eligible only when why is empty. Two details matter more than the list itself:
-
An exception is a reason to spare, not a pass. If asking "is it awaiting input?" throws, the answer recorded is
input-unknown, and the session stays. Unknown is never idle. -
Reasons are logged, not just the verdict. When someone asks "why didn't it free that one?", the log says
unreadorbackground-work, notfalse.
Some reasons only Relaymote can see, not the app: a session that owns an unfinished goal, or a master that is parked waiting on others. Those ids are collected outside and passed in as a protect list.
4. Some exclusions are permanent, not settings
A session on Remote Control is never released, and there is deliberately no option to change that. Teardown clears its Remote Control binding: the session disappears from the phone, nothing on the phone can wake it, and a message from the desktop brings it back on a new bridge id that orphans the old entry. Version 0.2.4 had a setting to allow it; 0.2.5 removed the setting, and the CLI now prints a note if someone passes the old flag.
If an exception protects against a failure the user cannot undo, don't make it configurable. A flag is an invitation.
5. Ship it dry, and let it go live by itself
The reaper is off by default. When someone turns it on, it does not start releasing. For its first 24 hours it runs in dry mode and only writes WOULD-REAP rows to a bounded log, then switches to live on its own:
const mode = c.mode === 'off' ? 'off' : c.mode === 'live' ? 'live' : c.mode === 'dry' ? 'dry'
: (c.dryUntil && now >= c.dryUntil) ? 'live' : 'dry';
Two small rules keep that honest:
- The dry clock starts on the background daemon's first tick, never on a manual
relaymote reaperrun. Trying it by hand doesn't shorten the observation window. - A mode written to the state file (
dry,liveoroff) pins it, so anyone who is not convinced after 24 hours can keep it dry.
The cost of this is one day of delay. In exchange, the first real release happens only after a day of logged decisions that anyone can read.
6. Measure before you act, and cap each pass
Each pass evaluates once without acting, records each eligible CLI's memory (process plus its whole child tree; on Windows via Get-CimInstance Win32_Process), and only then runs the acting pass. After teardown the pid is gone, so measuring afterwards would report nothing. At most six sessions are released per tick, so a wrong assumption can only do a bounded amount of damage before someone notices.
And if the script evaluated inside the app throws, the pass fails loudly (PAGE_THREW). It is never read as "there were no sessions".
What carries over
- Clean up through the owner's own teardown path, not an external kill.
- Tie your threshold to a measurable point where keeping the resource stops paying.
- Collect reasons to spare, treat "unknown" as a reason, and log the reasons.
- Make irreversible exceptions permanent; don't offer a flag.
- Ship destructive automation in dry-run with a self-ending observation window, and cap each pass.
Relaymote is MIT-licensed: github.com/ashwarsadh/relaymote. The reaper is lib/reaper.js and is off by default. Unofficial; not affiliated with or endorsed by Anthropic.
來源:DEV Community · dev.to