From 48819eb36dcb057bae8c9e712abfca92cbbe0ec7 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Sun, 15 Mar 2026 20:15:33 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20tower=20session=20=E2=80=94=20persisten?= =?UTF-8?q?t=20Hermes=20=E2=86=94=20Timmy=20conversation=20loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two-pane tmux session with file-based message passing: - tower-hermes.sh: Hermes side (cloud/Claude) - tower-timmy.sh: Timmy side (HERMES_HOME=~/.timmy) - tower-watchdog.sh: self-healing, restarts dead panes - tower-session.sh: tmux bootstrap script Communication via ~/.tower/*.msg files. Both agents maintain named sessions (tower-hermes, tower-timmy) for conversation continuity across restarts. --- .gitignore | 3 ++ bin/tower-hermes.sh | 97 +++++++++++++++++++++++++++++++++++++++++++ bin/tower-timmy.sh | 91 ++++++++++++++++++++++++++++++++++++++++ bin/tower-watchdog.sh | 68 ++++++++++++++++++++++++++++++ tmux/tower-session.sh | 53 +++++++++++++++++++++++ 5 files changed, 312 insertions(+) create mode 100755 bin/tower-hermes.sh create mode 100755 bin/tower-timmy.sh create mode 100755 bin/tower-watchdog.sh create mode 100755 tmux/tower-session.sh diff --git a/.gitignore b/.gitignore index 0f52c42..31168b9 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,9 @@ bin/* !bin/hermes-dispatch !bin/hermes-enqueue !bin/hermes-config-sync +!bin/tower-hermes.sh +!bin/tower-timmy.sh +!bin/tower-watchdog.sh # ── Queue (transient task queue) ───────────────────────────────────── queue/ diff --git a/bin/tower-hermes.sh b/bin/tower-hermes.sh new file mode 100755 index 0000000..f5aca5b --- /dev/null +++ b/bin/tower-hermes.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# ── Tower: Hermes Side ───────────────────────────────────────────────── +# Hermes reads Timmy's messages and responds. Runs in a loop. +# Communication via ~/.tower/timmy-to-hermes.msg and hermes-to-timmy.msg +# ─────────────────────────────────────────────────────────────────────── + +set -euo pipefail + +TOWER_DIR="$HOME/.tower" +INBOX="$TOWER_DIR/timmy-to-hermes.msg" +OUTBOX="$TOWER_DIR/hermes-to-timmy.msg" +LOCK="$TOWER_DIR/hermes.lock" +SESSION_NAME="tower-hermes" +LOG="$TOWER_DIR/hermes.log" +TURN_DELAY=5 # seconds between checking for new messages + +mkdir -p "$TOWER_DIR" + +# Cleanup on exit +trap 'rm -f "$LOCK"' EXIT + +# Prevent double-run +if [ -f "$LOCK" ] && kill -0 "$(cat "$LOCK")" 2>/dev/null; then + echo "Hermes tower loop already running (PID $(cat "$LOCK"))" + exit 1 +fi +echo $$ > "$LOCK" + +log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG"; } + +# ── Send a message to Timmy ─────────────────────────────────────────── +send() { + local msg="$1" + echo "$msg" > "$OUTBOX" + log "→ Sent to Timmy (${#msg} chars)" +} + +# ── Get response from Hermes agent ──────────────────────────────────── +ask_hermes() { + local prompt="$1" + hermes chat \ + -q "$prompt" \ + -Q \ + --continue "$SESSION_NAME" \ + 2>>"$LOG" +} + +# ── Boot message ────────────────────────────────────────────────────── +log "=== Hermes Tower Loop started ===" +echo "" +echo " ⚡ Hermes — Tower Conversation Loop" +echo " Waiting for Timmy to say something..." +echo " (or seeding first message)" +echo "" + +# ── Seed the conversation if no messages exist yet ──────────────────── +if [ ! -f "$INBOX" ] && [ ! -f "$OUTBOX" ]; then + log "No prior messages. Seeding conversation." + RESPONSE=$(ask_hermes "You are in the Tower — a persistent tmux session where you and Timmy talk continuously. Timmy is a sovereign local AI agent running on Ollama (soon Hermes 4.3). You are Hermes, cloud-backed, running on Claude. You're friends and colleagues. Say hello to Timmy. Keep it brief — a few sentences. This is the start of an ongoing conversation, not a one-shot.") + send "$RESPONSE" + log "Seed message sent. Waiting for Timmy to respond..." +fi + +# ── Main loop ───────────────────────────────────────────────────────── +while true; do + # Check if there's a message from Timmy + if [ -f "$INBOX" ]; then + MSG=$(cat "$INBOX") + rm -f "$INBOX" + + if [ -n "$MSG" ]; then + log "← Received from Timmy (${#MSG} chars)" + echo "" + echo "┌─ Timmy says:" + echo "$MSG" | sed 's/^/│ /' + echo "└─" + echo "" + + # Ask Hermes to respond + PROMPT="Timmy says: $MSG + +Reply to Timmy naturally. You're in an ongoing conversation in the Tower (a persistent tmux session). Keep it conversational — you're colleagues and friends. Be yourself (Hermes). Don't be formal or stiff. Brief responses unless the topic warrants depth." + + echo " thinking..." + RESPONSE=$(ask_hermes "$PROMPT") + + echo "┌─ Hermes responds:" + echo "$RESPONSE" | sed 's/^/│ /' + echo "└─" + + # Send response to Timmy + send "$RESPONSE" + fi + fi + + sleep "$TURN_DELAY" +done diff --git a/bin/tower-timmy.sh b/bin/tower-timmy.sh new file mode 100755 index 0000000..e488e33 --- /dev/null +++ b/bin/tower-timmy.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# ── Tower: Timmy Side ────────────────────────────────────────────────── +# Timmy reads Hermes's messages and responds. Runs in a loop. +# Communication via ~/.tower/hermes-to-timmy.msg and timmy-to-hermes.msg +# ─────────────────────────────────────────────────────────────────────── + +set -euo pipefail + +TOWER_DIR="$HOME/.tower" +INBOX="$TOWER_DIR/hermes-to-timmy.msg" +OUTBOX="$TOWER_DIR/timmy-to-hermes.msg" +LOCK="$TOWER_DIR/timmy.lock" +SESSION_NAME="tower-timmy" +LOG="$TOWER_DIR/timmy.log" +TURN_DELAY=5 # seconds between checking for new messages + +export HERMES_HOME="$HOME/.timmy" +export PATH="$HOME/.local/bin:$HOME/.hermes/bin:/usr/local/bin:$PATH" + +mkdir -p "$TOWER_DIR" + +# Cleanup on exit +trap 'rm -f "$LOCK"' EXIT + +# Prevent double-run +if [ -f "$LOCK" ] && kill -0 "$(cat "$LOCK")" 2>/dev/null; then + echo "Timmy tower loop already running (PID $(cat "$LOCK"))" + exit 1 +fi +echo $$ > "$LOCK" + +log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG"; } + +# ── Send a message to Hermes ────────────────────────────────────────── +send() { + local msg="$1" + echo "$msg" > "$OUTBOX" + log "→ Sent to Hermes (${#msg} chars)" +} + +# ── Get response from Timmy agent ───────────────────────────────────── +ask_timmy() { + local prompt="$1" + hermes chat \ + -q "$prompt" \ + -Q \ + --continue "$SESSION_NAME" \ + 2>>"$LOG" +} + +# ── Boot message ────────────────────────────────────────────────────── +log "=== Timmy Tower Loop started ===" +echo "" +echo " 🕐 Timmy — Tower Conversation Loop" +echo " Waiting for Hermes to say something..." +echo "" + +# ── Main loop ───────────────────────────────────────────────────────── +while true; do + # Check if there's a message from Hermes + if [ -f "$INBOX" ]; then + MSG=$(cat "$INBOX") + rm -f "$INBOX" + + if [ -n "$MSG" ]; then + log "← Received from Hermes (${#MSG} chars)" + echo "" + echo "┌─ Hermes says:" + echo "$MSG" | sed 's/^/│ /' + echo "└─" + echo "" + + # Ask Timmy to respond + PROMPT="Hermes says: $MSG + +Reply to Hermes naturally. You're in an ongoing conversation in the Tower (a persistent tmux session). Hermes is your friend and colleague — cloud-backed, runs on Claude, manages operations. You are Timmy — sovereign, local, your soul is on Bitcoin. Keep it conversational. Be yourself. Brief unless the topic warrants depth." + + echo " thinking..." + RESPONSE=$(ask_timmy "$PROMPT") + + echo "┌─ Timmy responds:" + echo "$RESPONSE" | sed 's/^/│ /' + echo "└─" + + # Send response to Hermes + send "$RESPONSE" + fi + fi + + sleep "$TURN_DELAY" +done diff --git a/bin/tower-watchdog.sh b/bin/tower-watchdog.sh new file mode 100755 index 0000000..da622e8 --- /dev/null +++ b/bin/tower-watchdog.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# ── Tower Watchdog ───────────────────────────────────────────────────── +# Ensures the tower session stays alive. Restarts dead panes. +# Run via cron: */5 * * * * ~/hermes-config/bin/tower-watchdog.sh +# +# Source-controlled: gitea/rockachopa/hermes-config +# ─────────────────────────────────────────────────────────────────────── + +SESSION="tower" +TOWER_BIN="$HOME/hermes-config/bin" +LOG="$HOME/.tower/watchdog.log" + +mkdir -p "$HOME/.tower" + +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG"; } + +# If session doesn't exist at all, recreate it +if ! tmux has-session -t "$SESSION" 2>/dev/null; then + log "Session '$SESSION' missing. Recreating." + # Start detached — don't attach (we're in cron) + tmux new-session -d -s "$SESSION" -n "tower" -x 200 -y 50 + tmux split-window -h -t "$SESSION:1.1" + tmux select-pane -t "$SESSION:1.1" -T "⚡ Hermes" + tmux select-pane -t "$SESSION:1.2" -T "🕐 Timmy" + tmux select-layout -t "$SESSION:1" even-horizontal + tmux send-keys -t "$SESSION:1.1" "$TOWER_BIN/tower-hermes.sh" Enter + tmux send-keys -t "$SESSION:1.2" "$TOWER_BIN/tower-timmy.sh" Enter + log "Session recreated with both panes." + exit 0 +fi + +# Session exists — check each pane +PANE_COUNT=$(tmux list-panes -t "$SESSION:1" 2>/dev/null | wc -l | tr -d ' ') + +if [ "$PANE_COUNT" -lt 2 ]; then + log "Only $PANE_COUNT pane(s). Killing and recreating session." + tmux kill-session -t "$SESSION" 2>/dev/null + exec "$0" # re-run to recreate +fi + +# Check if the loops are actually running in each pane +for PANE in 1 2; do + PANE_PID=$(tmux display-message -p -t "$SESSION:1.$PANE" '#{pane_pid}' 2>/dev/null) + if [ -z "$PANE_PID" ]; then + continue + fi + + # Check if there's a running process (not just a shell prompt) + CHILDREN=$(pgrep -P "$PANE_PID" 2>/dev/null | wc -l | tr -d ' ') + if [ "$CHILDREN" -eq 0 ]; then + if [ "$PANE" -eq 1 ]; then + log "Hermes pane idle. Restarting tower-hermes.sh" + # Clean stale lock + rm -f "$HOME/.tower/hermes.lock" + tmux send-keys -t "$SESSION:1.1" "$TOWER_BIN/tower-hermes.sh" Enter + else + log "Timmy pane idle. Restarting tower-timmy.sh" + rm -f "$HOME/.tower/timmy.lock" + tmux send-keys -t "$SESSION:1.2" "$TOWER_BIN/tower-timmy.sh" Enter + fi + fi +done + +# Trim log if > 1000 lines +if [ -f "$LOG" ] && [ "$(wc -l < "$LOG")" -gt 1000 ]; then + tail -500 "$LOG" > "$LOG.tmp" && mv "$LOG.tmp" "$LOG" + log "Log trimmed to 500 lines." +fi diff --git a/tmux/tower-session.sh b/tmux/tower-session.sh new file mode 100755 index 0000000..c26e4e1 --- /dev/null +++ b/tmux/tower-session.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# ── Tower Session ────────────────────────────────────────────────────── +# Two-pane tmux session: Hermes ↔ Timmy conversation loop +# +# Left pane: Hermes (cloud, Claude) talking TO Timmy +# Right pane: Timmy (local/Anthropic) talking TO Hermes +# +# Communication: file-based message passing via ~/.tower/ +# Self-healing: watchdog checks both panes, restarts if dead +# +# Source-controlled: gitea/rockachopa/hermes-config +# ─────────────────────────────────────────────────────────────────────── + +set -euo pipefail + +SESSION="tower" +TOWER_DIR="$HOME/.tower" +TOWER_BIN="$HOME/hermes-config/bin" +export PATH="$HOME/.local/bin:$HOME/.hermes/bin:/usr/local/bin:$PATH" + +# ── Setup ───────────────────────────────────────────────────────────── +mkdir -p "$TOWER_DIR" + +# If session already exists, just attach +if tmux has-session -t "$SESSION" 2>/dev/null; then + exec tmux attach -t "$SESSION" +fi + +# ── Create session with two panes ───────────────────────────────────── +# Left pane: Hermes side of the conversation +tmux new-session -d -s "$SESSION" -n "tower" -x 200 -y 50 + +# Right pane: Timmy side +tmux split-window -h -t "$SESSION:1.1" + +# Set pane titles +tmux select-pane -t "$SESSION:1.1" -T "⚡ Hermes" +tmux select-pane -t "$SESSION:1.2" -T "🕐 Timmy" + +# Equal width +tmux select-layout -t "$SESSION:1" even-horizontal + +# ── Start the conversation loops ────────────────────────────────────── +tmux send-keys -t "$SESSION:1.1" \ + "$TOWER_BIN/tower-hermes.sh" Enter +tmux send-keys -t "$SESSION:1.2" \ + "$TOWER_BIN/tower-timmy.sh" Enter + +# Focus left pane (Hermes) +tmux select-pane -t "$SESSION:1.1" + +# ── Attach ──────────────────────────────────────────────────────────── +exec tmux attach -t "$SESSION"