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']]
|
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):
|
def _vars_list_validate(raw):
|
||||||
if raw not in ({}, None):
|
if raw not in ({}, None):
|
||||||
raise OpError('vars.list takes no required args')
|
raise OpError('vars.list takes no required args')
|
||||||
@@ -1174,6 +1195,11 @@ OPS = {
|
|||||||
'timeout': 15, 'side_effecting': True,
|
'timeout': 15, 'side_effecting': True,
|
||||||
'desc': 'Kill a session on shared Muse tmux socket (/tmp/tmux-muse.sock)',
|
'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': {
|
'exec.ping': {
|
||||||
'validate': _health_validate,
|
'validate': _health_validate,
|
||||||
'build': lambda a: ['/bin/echo', 'PONG'],
|
'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)
|
print(f"Error: {err.strip() or out.strip()}", file=sys.stderr)
|
||||||
return rc
|
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):
|
def cmd_attach(args):
|
||||||
session = args.session
|
session = args.session
|
||||||
cmd = [TMUX_BIN, "-S", SOCKET_PATH, "attach", "-t", 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.add_argument("session", help="Session name")
|
||||||
p_kill.set_defaults(func=cmd_kill)
|
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
|
# attach
|
||||||
p_att = subparsers.add_parser("attach", help="Attach to a session interactively")
|
p_att = subparsers.add_parser("attach", help="Attach to a session interactively")
|
||||||
p_att.add_argument("session", help="Session name")
|
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 "*-smoke.log" -mtime +7 -delete 2>/dev/null
|
||||||
find /tmp -name "otp_*.py" -mtime +1 -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
|
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
|
# Kill a session
|
||||||
muse <account> tmux kill <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:
|
### 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.send {"session": "build-worker", "keys": "ls -la"}]
|
||||||
[TOOL tmux.capture {"session": "build-worker", "lines": 20}]
|
[TOOL tmux.capture {"session": "build-worker", "lines": 20}]
|
||||||
[TOOL tmux.list {}]
|
[TOOL tmux.list {}]
|
||||||
|
[TOOL tmux.prune {"ttl": 7200}]
|
||||||
[TOOL tmux.kill {"session": "build-worker"}]
|
[TOOL tmux.kill {"session": "build-worker"}]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user