226 lines
6.5 KiB
Bash
Executable File
226 lines
6.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Inbox Watcher — Agent Orchestrator Mailbox Polling Service
|
|
#
|
|
# Polls a session's inbox directory for new messages and delivers them
|
|
# to the agent via tmux send-keys. This runs in the background for each
|
|
# session and bridges the gap between file-based messaging and agent input.
|
|
#
|
|
# Environment variables:
|
|
# AO_SESSION - Session ID (e.g. "ao-10")
|
|
# AO_DATA_DIR - Base data directory (default: ~/.ao-sessions)
|
|
# POLL_INTERVAL - Polling interval in seconds (default: 5)
|
|
#
|
|
# Usage:
|
|
# AO_SESSION=ao-10 ~/scripts/inbox-watcher.sh &
|
|
#
|
|
|
|
set -euo pipefail
|
|
|
|
# Configuration
|
|
AO_DATA_DIR="${AO_DATA_DIR:-$HOME/.ao-sessions}"
|
|
POLL_INTERVAL="${POLL_INTERVAL:-5}"
|
|
|
|
# Validate required environment variables
|
|
if [[ -z "${AO_SESSION:-}" ]]; then
|
|
echo "ERROR: AO_SESSION environment variable not set" >&2
|
|
exit 1
|
|
fi
|
|
|
|
INBOX="$AO_DATA_DIR/$AO_SESSION/inbox"
|
|
SESSION_NAME="$AO_SESSION"
|
|
|
|
# Ensure inbox directory exists
|
|
if [[ ! -d "$INBOX" ]]; then
|
|
echo "ERROR: Inbox directory not found: $INBOX" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Check if tmux session exists
|
|
if ! tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
|
|
echo "ERROR: tmux session not found: $SESSION_NAME" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Inbox watcher started for session: $SESSION_NAME"
|
|
echo "Inbox: $INBOX"
|
|
echo "Poll interval: ${POLL_INTERVAL}s"
|
|
|
|
# Main polling loop
|
|
while true; do
|
|
# Find unprocessed messages (sorted by filename/timestamp)
|
|
messages=$(find "$INBOX" -maxdepth 1 -name "*.json" -type f 2>/dev/null | sort || true)
|
|
|
|
for msg_file in $messages; do
|
|
echo "Processing message: $(basename "$msg_file")"
|
|
|
|
# Parse message using jq (falls back to grep if jq not available)
|
|
if command -v jq &>/dev/null; then
|
|
msg_id=$(jq -r '.id' "$msg_file" 2>/dev/null || echo "unknown")
|
|
msg_type=$(jq -r '.type' "$msg_file" 2>/dev/null || echo "unknown")
|
|
msg_from=$(jq -r '.from' "$msg_file" 2>/dev/null || echo "unknown")
|
|
msg_priority=$(jq -r '.priority' "$msg_file" 2>/dev/null || echo "normal")
|
|
requires_ack=$(jq -r '.requiresAck' "$msg_file" 2>/dev/null || echo "false")
|
|
else
|
|
# Fallback: basic grep parsing
|
|
msg_id=$(grep -o '"id"[[:space:]]*:[[:space:]]*"[^"]*"' "$msg_file" | cut -d'"' -f4 || echo "unknown")
|
|
msg_type=$(grep -o '"type"[[:space:]]*:[[:space:]]*"[^"]*"' "$msg_file" | cut -d'"' -f4 || echo "unknown")
|
|
msg_from=$(grep -o '"from"[[:space:]]*:[[:space:]]*"[^"]*"' "$msg_file" | cut -d'"' -f4 || echo "unknown")
|
|
msg_priority="normal"
|
|
requires_ack="false"
|
|
fi
|
|
|
|
# Format message for agent based on type
|
|
prompt=""
|
|
case "$msg_type" in
|
|
fix_ci_failure)
|
|
if command -v jq &>/dev/null; then
|
|
pr=$(jq -r '.payload.pr // "Unknown"' "$msg_file")
|
|
check=$(jq -r '.payload.check // "Unknown"' "$msg_file")
|
|
error=$(jq -r '.payload.error // "Unknown"' "$msg_file")
|
|
else
|
|
pr="Unknown"
|
|
check="Unknown"
|
|
error="See message file: $msg_file"
|
|
fi
|
|
|
|
prompt="🔧 CI FAILURE DETECTED
|
|
|
|
Your PR has a failing CI check. Please fix:
|
|
|
|
Error: $error
|
|
PR: $pr
|
|
Check: $check"
|
|
;;
|
|
|
|
fix_review_comments)
|
|
if command -v jq &>/dev/null; then
|
|
pr=$(jq -r '.payload.pr // "Unknown"' "$msg_file")
|
|
comments=$(jq -r '.payload.comments[]? | "- \(.path // "?"):\(.line // "?") - \(.body // "")"' "$msg_file" 2>/dev/null || echo "See message file for details")
|
|
else
|
|
pr="Unknown"
|
|
comments="See message file: $msg_file"
|
|
fi
|
|
|
|
prompt="📝 REVIEW COMMENTS
|
|
|
|
Your PR has unresolved review comments. Please address them:
|
|
|
|
$comments
|
|
|
|
PR: $pr"
|
|
;;
|
|
|
|
status_request)
|
|
prompt="📊 STATUS REQUEST
|
|
|
|
Please provide a status update on your current task.
|
|
|
|
Include: current branch, PR status, blockers, ETA."
|
|
;;
|
|
|
|
shutdown)
|
|
if command -v jq &>/dev/null; then
|
|
reason=$(jq -r '.payload.reason // "Unknown"' "$msg_file")
|
|
else
|
|
reason="Unknown"
|
|
fi
|
|
|
|
prompt="🛑 SHUTDOWN REQUEST
|
|
|
|
The orchestrator is requesting you to shut down.
|
|
|
|
Reason: $reason"
|
|
;;
|
|
|
|
ack)
|
|
# Acknowledgments are silent, just mark as processed
|
|
echo "Received acknowledgment: $msg_id"
|
|
;;
|
|
|
|
*)
|
|
# Generic message - try to extract text payload
|
|
if command -v jq &>/dev/null; then
|
|
text=$(jq -r '.payload.text // empty' "$msg_file")
|
|
if [[ -n "$text" ]]; then
|
|
prompt="📬 MESSAGE FROM $msg_from
|
|
|
|
$text"
|
|
else
|
|
# No text field, show raw payload
|
|
payload=$(jq -r '.payload' "$msg_file" 2>/dev/null || echo "{}")
|
|
prompt="📬 MESSAGE FROM $msg_from
|
|
|
|
$payload"
|
|
fi
|
|
else
|
|
prompt="📬 MESSAGE FROM $msg_from
|
|
|
|
See message file: $msg_file"
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
# Send prompt to agent via tmux (if not empty)
|
|
if [[ -n "$prompt" ]]; then
|
|
echo "Sending prompt to session $SESSION_NAME"
|
|
|
|
# Clear any partial input
|
|
tmux send-keys -t "$SESSION_NAME" C-u 2>/dev/null || true
|
|
|
|
# For multi-line prompts, use load-buffer + paste-buffer
|
|
# This is more reliable than send-keys with -l flag for long text
|
|
echo "$prompt" | tmux load-buffer - 2>/dev/null || true
|
|
tmux paste-buffer -t "$SESSION_NAME" 2>/dev/null || true
|
|
|
|
# Small delay to let tmux process the pasted text
|
|
sleep 0.3
|
|
|
|
# Press Enter to submit
|
|
tmux send-keys -t "$SESSION_NAME" Enter 2>/dev/null || true
|
|
|
|
echo "Prompt sent successfully"
|
|
fi
|
|
|
|
# Move message to processed/
|
|
mkdir -p "$INBOX/processed"
|
|
mv "$msg_file" "$INBOX/processed/" 2>/dev/null || true
|
|
echo "Moved message to processed/"
|
|
|
|
# Send acknowledgment (if required)
|
|
if [[ "$requires_ack" == "true" ]]; then
|
|
echo "Sending acknowledgment to $msg_from"
|
|
|
|
# Generate ack message
|
|
ack_id=$(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "ack-$RANDOM-$RANDOM")
|
|
ack_id=$(echo "$ack_id" | tr '[:upper:]' '[:lower:]')
|
|
timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
|
|
ack_file="$AO_DATA_DIR/$msg_from/inbox/${timestamp//:}-${ack_id}-ack.json"
|
|
|
|
cat > "$ack_file" << EOF
|
|
{
|
|
"id": "$ack_id",
|
|
"from": "$AO_SESSION",
|
|
"to": "$msg_from",
|
|
"timestamp": "$timestamp",
|
|
"type": "ack",
|
|
"priority": "normal",
|
|
"payload": {
|
|
"text": "Message received and displayed to agent"
|
|
},
|
|
"requiresAck": false,
|
|
"replyTo": "$msg_id"
|
|
}
|
|
EOF
|
|
|
|
echo "Acknowledgment sent: $ack_id"
|
|
fi
|
|
|
|
echo "---"
|
|
done
|
|
|
|
# Sleep before next poll
|
|
sleep "$POLL_INTERVAL"
|
|
done
|