feat(tmux): implement 2-hour inactivity session TTL reaper and prune command
This commit is contained in:
@@ -729,6 +729,27 @@ def _tmux_kill_build(a):
|
||||
return [sys.executable, os.path.join(BIN_DIR, 'muse-tmux.py'), 'kill', a['session']]
|
||||
|
||||
|
||||
def _tmux_prune_validate(raw):
|
||||
if not isinstance(raw, dict):
|
||||
raise OpError('args must be an object')
|
||||
allowed = {'ttl'}
|
||||
for k in raw:
|
||||
if k not in allowed:
|
||||
raise OpError(f'unknown arg: {k}')
|
||||
ttl = raw.get('ttl', 7200)
|
||||
try:
|
||||
ttl = int(ttl)
|
||||
if ttl < 60:
|
||||
ttl = 60
|
||||
except Exception:
|
||||
ttl = 7200
|
||||
return {'ttl': ttl}
|
||||
|
||||
|
||||
def _tmux_prune_build(a):
|
||||
return [sys.executable, os.path.join(BIN_DIR, 'muse-tmux.py'), 'prune', '--ttl', str(a['ttl'])]
|
||||
|
||||
|
||||
def _vars_list_validate(raw):
|
||||
if raw not in ({}, None):
|
||||
raise OpError('vars.list takes no required args')
|
||||
@@ -1174,6 +1195,11 @@ OPS = {
|
||||
'timeout': 15, 'side_effecting': True,
|
||||
'desc': 'Kill a session on shared Muse tmux socket (/tmp/tmux-muse.sock)',
|
||||
},
|
||||
'tmux.prune': {
|
||||
'validate': _tmux_prune_validate, 'build': _tmux_prune_build,
|
||||
'timeout': 15, 'side_effecting': True,
|
||||
'desc': 'Reap stale unattached sessions inactive for >TTL (default 2h)',
|
||||
},
|
||||
'exec.ping': {
|
||||
'validate': _health_validate,
|
||||
'build': lambda a: ['/bin/echo', 'PONG'],
|
||||
|
||||
@@ -105,6 +105,40 @@ def cmd_kill(args):
|
||||
print(f"Error: {err.strip() or out.strip()}", file=sys.stderr)
|
||||
return rc
|
||||
|
||||
def cmd_prune(args):
|
||||
ttl_seconds = getattr(args, "ttl", 7200) or 7200
|
||||
rc, out, err = _run_tmux("list-sessions", "-F", "#{session_name} #{session_activity} #{session_attached}")
|
||||
if rc != 0:
|
||||
if "no server running" in (err or "").lower() or rc == 1:
|
||||
print(f"No active tmux server on {SOCKET_PATH}.")
|
||||
return 0
|
||||
print(f"Error listing sessions: {err.strip() or out.strip()}", file=sys.stderr)
|
||||
return rc
|
||||
|
||||
now = int(time.time())
|
||||
reaped = []
|
||||
kept = []
|
||||
|
||||
for line in out.strip().splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) >= 3:
|
||||
s_name, s_activity, s_attached = parts[0], int(parts[1]), int(parts[2])
|
||||
idle_time = now - s_activity
|
||||
# Only prune if unattached and idle > TTL
|
||||
if s_attached == 0 and idle_time > ttl_seconds:
|
||||
_run_tmux("kill-session", "-t", s_name)
|
||||
reaped.append((s_name, idle_time))
|
||||
else:
|
||||
kept.append((s_name, idle_time, s_attached))
|
||||
|
||||
if reaped:
|
||||
print(f"✔ Reaped {len(reaped)} stale unattached session(s) (idle > {ttl_seconds//3600}h):")
|
||||
for s_name, idle in reaped:
|
||||
print(f" • {s_name} (idle: {idle//60}m)")
|
||||
else:
|
||||
print(f"No stale unattached sessions to reap (all active within {ttl_seconds//3600}h).")
|
||||
return 0
|
||||
|
||||
def cmd_attach(args):
|
||||
session = args.session
|
||||
cmd = [TMUX_BIN, "-S", SOCKET_PATH, "attach", "-t", session]
|
||||
@@ -144,6 +178,11 @@ def main():
|
||||
p_kill.add_argument("session", help="Session name")
|
||||
p_kill.set_defaults(func=cmd_kill)
|
||||
|
||||
# prune
|
||||
p_prune = subparsers.add_parser("prune", help="Reap stale unattached sessions inactive for >TTL (default: 7200s / 2h)")
|
||||
p_prune.add_argument("--ttl", type=int, default=7200, help="Inactivity threshold in seconds (default: 7200)")
|
||||
p_prune.set_defaults(func=cmd_prune)
|
||||
|
||||
# attach
|
||||
p_att = subparsers.add_parser("attach", help="Attach to a session interactively")
|
||||
p_att.add_argument("session", help="Session name")
|
||||
|
||||
@@ -22,3 +22,6 @@ done
|
||||
find /tmp -name "*-smoke.log" -mtime +7 -delete 2>/dev/null
|
||||
find /tmp -name "otp_*.py" -mtime +1 -delete 2>/dev/null
|
||||
find /tmp -name "check_*.py" -mtime +1 -delete 2>/dev/null
|
||||
|
||||
# Prune unattached stale agent tmux sessions older than 2 hours (7200s)
|
||||
python3 "$(cd "$(dirname "$0")" && pwd)/muse-tmux.py" prune --ttl 7200 2>/dev/null || true
|
||||
|
||||
@@ -89,6 +89,9 @@ muse <account> tmux capture <session> --lines 30
|
||||
|
||||
# Kill a session
|
||||
muse <account> tmux kill <session>
|
||||
|
||||
# Prune unattached sessions inactive for >2 hours (or custom --ttl in seconds)
|
||||
muse <account> tmux prune [--ttl 7200]
|
||||
```
|
||||
|
||||
### Via Autonomous Agent Directives:
|
||||
@@ -98,6 +101,7 @@ Agents can emit structured tool calls in sidechats:
|
||||
[TOOL tmux.send {"session": "build-worker", "keys": "ls -la"}]
|
||||
[TOOL tmux.capture {"session": "build-worker", "lines": 20}]
|
||||
[TOOL tmux.list {}]
|
||||
[TOOL tmux.prune {"ttl": 7200}]
|
||||
[TOOL tmux.kill {"session": "build-worker"}]
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user