feat: tower session — persistent Hermes ↔ Timmy conversation loop
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.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -62,6 +62,9 @@ bin/*
|
|||||||
!bin/hermes-dispatch
|
!bin/hermes-dispatch
|
||||||
!bin/hermes-enqueue
|
!bin/hermes-enqueue
|
||||||
!bin/hermes-config-sync
|
!bin/hermes-config-sync
|
||||||
|
!bin/tower-hermes.sh
|
||||||
|
!bin/tower-timmy.sh
|
||||||
|
!bin/tower-watchdog.sh
|
||||||
|
|
||||||
# ── Queue (transient task queue) ─────────────────────────────────────
|
# ── Queue (transient task queue) ─────────────────────────────────────
|
||||||
queue/
|
queue/
|
||||||
|
|||||||
97
bin/tower-hermes.sh
Executable file
97
bin/tower-hermes.sh
Executable file
@@ -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
|
||||||
91
bin/tower-timmy.sh
Executable file
91
bin/tower-timmy.sh
Executable file
@@ -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
|
||||||
68
bin/tower-watchdog.sh
Executable file
68
bin/tower-watchdog.sh
Executable file
@@ -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
|
||||||
53
tmux/tower-session.sh
Executable file
53
tmux/tower-session.sh
Executable file
@@ -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"
|
||||||
Reference in New Issue
Block a user