Implement tmux-state.sh: snapshots all tmux pane state to ~/.timmy/tmux-state.json and ~/.hermes/tmux-state.json every supervisor cycle. Per-pane tracking: - address, pane_id, pid, size, active state - command, title, tty - hermes profile, model, provider - session_id (for --resume) - task (last prompt extracted from pane content) - context_pct (estimated from pane content) Also implement tmux-resume.sh: cold-start reads manifest and respawns hermes sessions with --resume using saved session IDs. Closes #512
98 lines
3.1 KiB
Bash
Executable File
98 lines
3.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ── tmux-resume.sh — Cold-start Session Resume ───────────────────────────
|
|
# Reads ~/.timmy/tmux-state.json and resumes hermes sessions.
|
|
# Run at startup to restore pane state after supervisor restart.
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
|
|
set -euo pipefail
|
|
|
|
MANIFEST="${HOME}/.timmy/tmux-state.json"
|
|
|
|
if [ ! -f "$MANIFEST" ]; then
|
|
echo "[tmux-resume] No manifest found at $MANIFEST — starting fresh."
|
|
exit 0
|
|
fi
|
|
|
|
python3 << 'PYEOF'
|
|
import json, subprocess, os, sys
|
|
from datetime import datetime, timezone
|
|
|
|
MANIFEST = os.path.expanduser("~/.timmy/tmux-state.json")
|
|
|
|
def run(cmd):
|
|
try:
|
|
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30)
|
|
return r.stdout.strip(), r.returncode
|
|
except Exception as e:
|
|
return str(e), 1
|
|
|
|
def session_exists(name):
|
|
out, _ = run(f"tmux has-session -t '{name}' 2>&1")
|
|
return "can't find" not in out.lower()
|
|
|
|
with open(MANIFEST) as f:
|
|
state = json.load(f)
|
|
|
|
ts = state.get("timestamp", "unknown")
|
|
age = "unknown"
|
|
try:
|
|
t = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
delta = datetime.now(timezone.utc) - t
|
|
mins = int(delta.total_seconds() / 60)
|
|
if mins < 60:
|
|
age = f"{mins}m ago"
|
|
else:
|
|
age = f"{mins//60}h {mins%60}m ago"
|
|
except:
|
|
pass
|
|
|
|
print(f"[tmux-resume] Manifest from {age}: {state['summary']['total_sessions']} sessions, "
|
|
f"{state['summary']['hermes_panes']} hermes panes")
|
|
|
|
restored = 0
|
|
skipped = 0
|
|
|
|
for pane in state.get("panes", []):
|
|
if not pane.get("is_hermes"):
|
|
continue
|
|
|
|
addr = pane["address"] # e.g. "BURN:2.3"
|
|
session = addr.split(":")[0]
|
|
session_id = pane.get("session_id")
|
|
profile = pane.get("profile", "default")
|
|
model = pane.get("model", "")
|
|
task = pane.get("task", "")
|
|
|
|
# Skip if session already exists (already running)
|
|
if session_exists(session):
|
|
print(f" [skip] {addr} — session '{session}' already exists")
|
|
skipped += 1
|
|
continue
|
|
|
|
# Respawn hermes with session resume if we have a session ID
|
|
if session_id:
|
|
print(f" [resume] {addr} — profile={profile} model={model} session={session_id}")
|
|
cmd = f"hermes chat --resume {session_id}"
|
|
else:
|
|
print(f" [start] {addr} — profile={profile} model={model} (no session ID)")
|
|
cmd = f"hermes chat --profile {profile}"
|
|
|
|
# Create tmux session and run hermes
|
|
run(f"tmux new-session -d -s '{session}' -n '{session}:0'")
|
|
run(f"tmux send-keys -t '{session}' '{cmd}' Enter")
|
|
restored += 1
|
|
|
|
# Write resume log
|
|
log = {
|
|
"resumed_at": datetime.now(timezone.utc).isoformat(),
|
|
"manifest_age": age,
|
|
"restored": restored,
|
|
"skipped": skipped,
|
|
}
|
|
log_path = os.path.expanduser("~/.timmy/tmux-resume.log")
|
|
with open(log_path, "w") as f:
|
|
json.dump(log, f, indent=2)
|
|
|
|
print(f"[tmux-resume] Done: {restored} restored, {skipped} skipped")
|
|
PYEOF
|