diff --git a/.opencode/agents/async-agent-manager.md b/.opencode/agents/async-agent-manager.md index 749562d6b..6f410c0b9 100644 --- a/.opencode/agents/async-agent-manager.md +++ b/.opencode/agents/async-agent-manager.md @@ -49,6 +49,174 @@ You are the centralized manager for all async agent operations via the OpenCode All other agents MUST use you for any async operations. You handle all the complexity of the OpenCode API so other agents don't need to. +## Valid Agent Names + +The following agents are valid for async operations: +- general +- explore +- build +- plan +- implementation-worker +- implementation-orchestrator +- pr-self-reviewer +- continuous-pr-reviewer +- uat-tester +- bug-hunter +- test-infra-improver +- architect +- epic-planner +- human-liaison +- agent-evolver +- architecture-guard +- spec-updater +- backlog-groomer +- docs-writer +- timeline-updater +- project-owner +- system-watchdog +- quality-enforcer +- state-reconciler + +## Helper Functions + +### Retry with Exponential Backoff +```bash +function retry_with_backoff() { + local command="$1" + local description="$2" + local attempt=1 + local max_attempts=5 + local delays=(0 5 30 120 300) # 0s, 5s, 30s, 2m, 5m + + while [ $attempt -le $max_attempts ]; do + if [ $attempt -gt 1 ]; then + local delay=${delays[$((attempt-1))]} + echo "[RETRY] Attempt $attempt/$max_attempts for $description (waiting ${delay}s)" >&2 + sleep $delay + fi + + # Execute command and capture both output and exit code + local output + output=$(eval "$command" 2>&1) + local exit_code=$? + + if [ $exit_code -eq 0 ]; then + echo "$output" + return 0 + fi + + echo "[RETRY] Attempt $attempt failed: $output" >&2 + ((attempt++)) + done + + echo "[ERROR] All $max_attempts attempts failed for $description" >&2 + return 1 +} + +# Get detailed session status +function get_session_status() { + local session_id="$1" + local server_url="${2:-http://localhost:4096}" + + # First try the status endpoint + local status_response=$(retry_with_backoff \ + "curl -s '${server_url}/session/status'" \ + "get session status") + + if [ -n "$status_response" ] && [ "$status_response" != "null" ]; then + local status=$(echo "$status_response" | jq -r --arg id "$session_id" '.[$id] // empty') + if [ -n "$status" ] && [ "$status" != "null" ]; then + echo "$status" + return 0 + fi + fi + + # If not found in status, try direct session endpoint + local session_response=$(retry_with_backoff \ + "curl -s '${server_url}/session/${session_id}'" \ + "get session details") + + if [ -n "$session_response" ] && [ "$session_response" != "null" ]; then + # Try to determine status from session details + local has_messages=$(echo "$session_response" | jq -r '.messages // empty' | wc -l) + if [ "$has_messages" -gt 0 ]; then + echo "running" + else + echo "initializing" + fi + return 0 + fi + + echo "unknown" +} + +# Validate agent name +function validate_agent_name() { + local agent_name="$1" + local force="${2:-false}" + + local valid_agents=( + "general" "explore" "build" "plan" + "implementation-worker" "implementation-orchestrator" + "pr-self-reviewer" "continuous-pr-reviewer" + "uat-tester" "bug-hunter" "test-infra-improver" + "architect" "epic-planner" "human-liaison" + "agent-evolver" "architecture-guard" "spec-updater" + "backlog-groomer" "docs-writer" "timeline-updater" + "project-owner" "system-watchdog" + "quality-enforcer" "state-reconciler" + ) + + for valid in "${valid_agents[@]}"; do + if [ "$agent_name" = "$valid" ]; then + return 0 + fi + done + + if [ "$force" = "true" ]; then + echo "[WARNING] Agent '$agent_name' not in valid list, proceeding anyway (force=true)" >&2 + return 0 + fi + + echo "[ERROR] Invalid agent name: '$agent_name'" >&2 + echo "[ERROR] Valid agents: ${valid_agents[*]}" >&2 + return 1 +} + +# Format response consistently +function format_response() { + local status="$1" + local operation="$2" + local data="$3" + local suggestions="$4" + + if [ "$status" = "success" ]; then + echo "✅ **$operation completed successfully**" + elif [ "$status" = "error" ]; then + echo "❌ **$operation failed**" + elif [ "$status" = "warning" ]; then + echo "⚠️ **$operation completed with warnings**" + else + echo "ℹ️ **$operation status: $status**" + fi + + echo "" + + if [ -n "$data" ]; then + echo "**Details:**" + echo '```json' + echo "$data" + echo '```' + echo "" + fi + + if [ -n "$suggestions" ]; then + echo "**Suggested actions:**" + echo "$suggestions" + fi +} +``` + ## Core Operations ### 1. Start Async Agent @@ -60,27 +228,58 @@ All other agents MUST use you for any async operations. You handle all the compl - `prompt_text` - the prompt to send to the agent - `server_url` - OpenCode server URL (defaults to "http://localhost:4096") - `restart_existing` - whether to restart if session with same tag exists (defaults to false) +- `force` - force launch even with invalid agent name (defaults to false) **Process:** ```bash +# Validate agent name +if ! validate_agent_name "$agent_name" "$force"; then + format_response "error" "Agent validation" \ + "{\"reason\": \"invalid_agent_name\", \"agent\": \"$agent_name\"}" \ + "- Use a valid agent name from the list above +- Or set force=true to override validation" + exit 1 +fi + # Step 1: Check for existing sessions with this tag -existing_sessions=$(curl -s "${server_url}/session" | jq -r --arg tag "$tag" '.[] | select(.title | contains("[\($tag)]")) | .id') +existing_check=$(retry_with_backoff \ + "curl -s '${server_url}/session'" \ + "check existing sessions") + +if [ -z "$existing_check" ]; then + format_response "error" "Session check" \ + "{\"reason\": \"api_unreachable\", \"server\": \"$server_url\"}" \ + "- Check if OpenCode server is running +- Verify the server URL is correct +- Try again in a few moments" + exit 1 +fi + +existing_sessions=$(echo "$existing_check" | jq -r --arg tag "$tag" '.[] | select(.title | contains("[\($tag)]")) | .id') if [ -n "$existing_sessions" ] && [ "$restart_existing" != "true" ]; then - echo "{\"status\": \"skipped\", \"reason\": \"existing_session\", \"session_id\": \"$existing_sessions\"}" + session_id=$(echo "$existing_sessions" | head -1) + format_response "warning" "Session creation skipped" \ + "{\"status\": \"skipped\", \"reason\": \"existing_session\", \"session_id\": \"$session_id\", \"tag\": \"$tag\"}" \ + "- Use restart_existing=true to force create a new session +- Or use a different tag for the new session" exit 0 fi # Step 2: Create new session session_title="[$tag] $display_name" -session_response=$(curl -s -X POST "${server_url}/session" \ - -H "Content-Type: application/json" \ - -d "{\"title\": \"$session_title\"}") +create_response=$(retry_with_backoff \ + "curl -s -X POST '${server_url}/session' -H 'Content-Type: application/json' -d '{\"title\": \"$session_title\"}'" \ + "create session") -session_id=$(echo "$session_response" | jq -r '.id') +session_id=$(echo "$create_response" | jq -r '.id // empty') if [ -z "$session_id" ] || [ "$session_id" = "null" ]; then - echo "{\"status\": \"error\", \"reason\": \"failed_to_create_session\"}" + format_response "error" "Session creation" \ + "{\"reason\": \"failed_to_create_session\", \"response\": $create_response}" \ + "- Check API response for error details +- Verify server is accepting new sessions +- Try again with different parameters" exit 1 fi @@ -88,33 +287,44 @@ fi # Properly escape the prompt text for JSON escaped_prompt=$(echo "$prompt_text" | jq -Rs .) -launch_response=$(curl -s -w "\n%{http_code}" -X POST "${server_url}/session/${session_id}/prompt_async" \ - -H "Content-Type: application/json" \ - -d "{ - \"agent\": \"$agent_name\", - \"parts\": [{\"type\": \"text\", \"text\": $escaped_prompt}] - }") +launch_response=$(retry_with_backoff \ + "curl -s -w '\n%{http_code}' -X POST '${server_url}/session/${session_id}/prompt_async' \ + -H 'Content-Type: application/json' \ + -d '{\"agent\": \"$agent_name\", \"parts\": [{\"type\": \"text\", \"text\": $escaped_prompt}]}'" \ + "launch agent") http_code=$(echo "$launch_response" | tail -n1) response_body=$(echo "$launch_response" | sed '$d') # prompt_async returns 204 No Content on success if [ "$http_code" != "204" ] && [ "$http_code" != "200" ]; then - echo "{\"status\": \"error\", \"reason\": \"failed_to_launch_agent\", \"http_code\": \"$http_code\", \"details\": \"$response_body\"}" + format_response "error" "Agent launch" \ + "{\"reason\": \"failed_to_launch\", \"http_code\": \"$http_code\", \"session_id\": \"$session_id\", \"response\": \"$response_body\"}" \ + "- Session was created but agent launch failed +- Check if agent name is valid +- Verify prompt text is properly formatted" exit 1 fi # Return success -echo "{ - \"status\": \"success\", - \"session_id\": \"$session_id\", - \"tag\": \"$tag\", - \"agent_name\": \"$agent_name\", - \"display_name\": \"$display_name\", - \"session_title\": \"$session_title\", - \"server_url\": \"$server_url\", - \"launched_at\": \"$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)\" -}" +result_json=$(cat < /tmp/status_results.json + + format_response "success" "Session search" \ + "$(cat /tmp/status_results.json)" \ + "Found $(cat /tmp/status_results.json | jq '. | length') sessions matching pattern: $tag_pattern" + rm -f /tmp/status_results.json + fi else - # Return all statuses with titles - sessions=$(curl -s "${server_url}/session") - echo "$sessions" | jq -r '.[] | {id: .id, title: .title}' | jq -c '.' | while read -r session; do + # Return all sessions with status + echo "$sessions_response" | jq -r '.[] | {id: .id, title: .title}' | jq -c '.' | while read -r session; do sid=$(echo "$session" | jq -r '.id') - title=$(echo "$session" | jq -r '.title') - status=$(echo "$status_response" | jq -r --arg id "$sid" '.[$id] // "unknown"') - echo "{\"session_id\": \"$sid\", \"title\": \"$title\", \"status\": \"$status\"}" - done | jq -s '.' + status=$(get_session_status "$sid" "$server_url") + echo "$session" | jq --arg status "$status" '. + {status: $status}' + done | jq -s '.' > /tmp/all_status.json + + format_response "success" "All sessions" \ + "$(cat /tmp/all_status.json)" \ + "Total sessions: $(cat /tmp/all_status.json | jq '. | length')" + rm -f /tmp/all_status.json fi ``` @@ -164,21 +414,61 @@ fi **Parameters:** - `session_id` - ID of the session -- `limit` - maximum number of messages to retrieve (optional, default 50) +- `limit` - maximum number of messages to retrieve (optional, default: all messages) +- `offset` - number of messages to skip from the beginning (optional, default: 0) - `server_url` - OpenCode server URL (defaults to "http://localhost:4096") **Process:** ```bash -# Get messages for the session -messages_response=$(curl -s "${server_url}/session/${session_id}/message?limit=${limit:-50}") +# Build query parameters +query_params="" +if [ -n "$limit" ]; then + query_params="?limit=${limit}" + if [ -n "$offset" ]; then + query_params="${query_params}&offset=${offset}" + fi +elif [ -n "$offset" ]; then + query_params="?offset=${offset}" +fi -if [ -z "$messages_response" ] || [ "$messages_response" = "null" ]; then - echo "{\"status\": \"error\", \"reason\": \"session_not_found\"}" - exit 1 +# Get messages for the session (with retry and wait for empty responses) +messages_response=$(retry_with_backoff \ + "curl -s '${server_url}/session/${session_id}/message${query_params}'" \ + "get messages") + +if [ -z "$messages_response" ] || [ "$messages_response" = "null" ] || [ "$messages_response" = "[]" ]; then + # Wait and retry once for initializing sessions + echo "[INFO] No messages found, waiting 2s for session initialization..." >&2 + sleep 2 + + messages_response=$(curl -s "${server_url}/session/${session_id}/message${query_params}") + + if [ -z "$messages_response" ] || [ "$messages_response" = "null" ] || [ "$messages_response" = "[]" ]; then + # Check if session exists + session_status=$(get_session_status "$session_id" "$server_url") + + if [ "$session_status" = "unknown" ]; then + format_response "error" "Message retrieval" \ + "{\"reason\": \"session_not_found\", \"session_id\": \"$session_id\"}" \ + "- Verify session ID is correct +- Session may have been deleted" + elif [ "$session_status" = "initializing" ]; then + format_response "warning" "Message retrieval" \ + "{\"reason\": \"session_initializing\", \"session_id\": \"$session_id\", \"status\": \"$session_status\"}" \ + "- Session is still initializing +- Try again in a few moments" + else + format_response "warning" "Message retrieval" \ + "{\"reason\": \"no_messages\", \"session_id\": \"$session_id\", \"status\": \"$session_status\"}" \ + "- Session exists but has no messages yet +- Agent may not have started processing" + fi + exit 0 + fi fi # Format messages for readability -echo "$messages_response" | jq ' +formatted_messages=$(echo "$messages_response" | jq ' map({ id: .info.id, role: .info.role, @@ -188,43 +478,160 @@ echo "$messages_response" | jq ' tool_calls: (.parts | map(select(.type == "tool_call")) | length), has_error: (.parts | map(select(.type == "error")) | length > 0) }) -' +') + +message_count=$(echo "$formatted_messages" | jq '. | length') +total_messages=$(echo "$messages_response" | jq '. | length') + +pagination_info="" +if [ -n "$limit" ] || [ -n "$offset" ]; then + pagination_info=" +Pagination: showing $message_count messages" + [ -n "$offset" ] && pagination_info="$pagination_info (skipped first $offset)" + [ -n "$limit" ] && pagination_info="$pagination_info (limited to $limit)" +fi + +format_response "success" "Message retrieval" \ + "$formatted_messages" \ + "Retrieved $message_count messages from session $session_id$pagination_info" ``` -### 4. Search Sessions by Tag +### 4. Search Sessions **Parameters:** -- `tag_pattern` - pattern to search for in session titles +- `search_pattern` - search pattern with format "type:value" or simple tag search + - `tag:AUTO-IMP*` - wildcard tag search + - `agent:implementation-worker` - search by agent name + - `status:running` - search by status + - `age:>1h` - search by age (>1h, <30m, etc.) +- `combine_filters` - how to combine multiple filters (AND/OR, default: AND) - `include_status` - whether to include session status (optional, default true) - `server_url` - OpenCode server URL (defaults to "http://localhost:4096") **Process:** ```bash # Get all sessions -sessions=$(curl -s "${server_url}/session") +sessions=$(retry_with_backoff \ + "curl -s '${server_url}/session'" \ + "get sessions") -# Filter by tag pattern -matching=$(echo "$sessions" | jq -r --arg pattern "$tag_pattern" ' - .[] | select(.title | contains("[" + $pattern)) | - { - id: .id, - title: .title, - created_at: .created_at, - updated_at: .updated_at - } -') +if [ -z "$sessions" ] || [ "$sessions" = "null" ]; then + format_response "error" "Session search" \ + "{\"reason\": \"api_unreachable\"}" \ + "- Check if OpenCode server is running" + exit 1 +fi -if [ "$include_status" != "false" ]; then - # Get status for each matching session - status_response=$(curl -s "${server_url}/session/status") +# Parse search patterns +IFS=',' read -ra patterns <<< "$search_pattern" +matching_sessions="$sessions" + +for pattern in "${patterns[@]}"; do + pattern=$(echo "$pattern" | xargs) # trim whitespace - echo "$matching" | jq -c '.' | while read -r session; do + if [[ "$pattern" == *":"* ]]; then + # Structured search + search_type="${pattern%%:*}" + search_value="${pattern#*:}" + + case "$search_type" in + "tag") + # Convert wildcard to regex + regex_pattern=$(echo "$search_value" | sed 's/\*/.*/g') + matching_sessions=$(echo "$matching_sessions" | jq -r --arg pattern "$regex_pattern" ' + .[] | select(.title | match("\\[" + $pattern + "\\]"; "i")) + ') + ;; + "agent") + # Search in messages for agent name + temp_matches=() + echo "$matching_sessions" | jq -c '.[]' | while read -r session; do + sid=$(echo "$session" | jq -r '.id') + # Check first message for agent + first_msg=$(curl -s "${server_url}/session/${sid}/message?limit=1" | jq -r '.[0].info.agent // empty') + if [[ "$first_msg" == *"$search_value"* ]]; then + temp_matches+=("$session") + fi + done + matching_sessions=$(printf '%s\n' "${temp_matches[@]}" | jq -s '.') + ;; + "status") + # Filter by status + temp_matches=() + echo "$matching_sessions" | jq -c '.[]' | while read -r session; do + sid=$(echo "$session" | jq -r '.id') + status=$(get_session_status "$sid" "$server_url") + if [ "$status" = "$search_value" ]; then + temp_matches+=("$session") + fi + done + matching_sessions=$(printf '%s\n' "${temp_matches[@]}" | jq -s '.') + ;; + "age") + # Parse age filter (>1h, <30m, etc.) + operator="${search_value:0:1}" + age_value="${search_value:1}" + + # Convert to seconds + if [[ "$age_value" == *"h" ]]; then + age_seconds=$((${age_value%h} * 3600)) + elif [[ "$age_value" == *"m" ]]; then + age_seconds=$((${age_value%m} * 60)) + else + age_seconds=$age_value + fi + + current_time=$(date +%s) + temp_matches=() + echo "$matching_sessions" | jq -c '.[]' | while read -r session; do + created_at=$(echo "$session" | jq -r '.created_at') + session_age=$((current_time - created_at / 1000)) + + if [ "$operator" = ">" ] && [ $session_age -gt $age_seconds ]; then + temp_matches+=("$session") + elif [ "$operator" = "<" ] && [ $session_age -lt $age_seconds ]; then + temp_matches+=("$session") + fi + done + matching_sessions=$(printf '%s\n' "${temp_matches[@]}" | jq -s '.') + ;; + esac + else + # Simple tag search (backward compatibility) + matching_sessions=$(echo "$matching_sessions" | jq -r --arg pattern "$pattern" ' + .[] | select(.title | contains("[" + $pattern)) + ') + fi + + # For OR logic, we'd need to accumulate matches instead + if [ "$combine_filters" = "OR" ]; then + echo "[WARNING] OR filter combination not yet implemented, using AND" >&2 + fi +done + +# Add status if requested +if [ "$include_status" != "false" ] && [ -n "$matching_sessions" ] && [ "$matching_sessions" != "[]" ]; then + echo "$matching_sessions" | jq -c '.' | while read -r session; do sid=$(echo "$session" | jq -r '.id') - status=$(echo "$status_response" | jq -r --arg id "$sid" '.[$id] // "unknown"') + status=$(get_session_status "$sid" "$server_url") echo "$session" | jq --arg status "$status" '. + {status: $status}' - done | jq -s '.' + done | jq -s '.' > /tmp/search_results.json + matching_sessions=$(cat /tmp/search_results.json) + rm -f /tmp/search_results.json +fi + +# Format results +if [ -z "$matching_sessions" ] || [ "$matching_sessions" = "[]" ]; then + format_response "warning" "Session search" \ + "{\"search_pattern\": \"$search_pattern\", \"found\": 0}" \ + "- No sessions found matching: $search_pattern +- Try different search criteria +- Sessions may have been cleaned up" else - echo "$matching" | jq -s '.' + match_count=$(echo "$matching_sessions" | jq '. | length') + format_response "success" "Session search" \ + "$matching_sessions" \ + "Found $match_count sessions matching: $search_pattern" fi ``` @@ -234,68 +641,111 @@ fi - `session_id` - ID of the session to close (optional) - `tag_pattern` - pattern to match sessions for bulk cleanup (optional) - `only_completed` - only close completed sessions (optional, default true) +- `force` - force deletion regardless of status (optional, default false) - `server_url` - OpenCode server URL (defaults to "http://localhost:4096") **Process:** ```bash # Determine which sessions to close if [ -n "$session_id" ]; then - sessions_to_close="[$session_id]" + # Single session + sessions_to_check=$(retry_with_backoff \ + "curl -s '${server_url}/session/${session_id}'" \ + "get session") + + if [ -z "$sessions_to_check" ] || [ "$sessions_to_check" = "null" ]; then + format_response "error" "Session cleanup" \ + "{\"reason\": \"session_not_found\", \"session_id\": \"$session_id\"}" \ + "- Verify session ID is correct +- Session may already be deleted" + exit 1 + fi + + sessions_to_close="[{\"id\": \"$session_id\", \"title\": \"$(echo "$sessions_to_check" | jq -r '.title // "Unknown"')\"}]" elif [ -n "$tag_pattern" ]; then # Find sessions matching the pattern - all_sessions=$(curl -s "${server_url}/session") + all_sessions=$(retry_with_backoff \ + "curl -s '${server_url}/session'" \ + "get sessions") + sessions_to_close=$(echo "$all_sessions" | jq -r --arg pattern "$tag_pattern" ' - [.[] | select(.title | contains("[" + $pattern)) | .id] + [.[] | select(.title | contains("[" + $pattern)) | {id: .id, title: .title}] ') else - echo "{\"status\": \"error\", \"reason\": \"no_session_specified\"}" + format_response "error" "Session cleanup" \ + "{\"reason\": \"no_target_specified\"}" \ + "- Specify either session_id or tag_pattern +- Use tag_pattern='*' to clean all sessions" exit 1 fi -# Get status if we need to filter by completion -if [ "$only_completed" != "false" ]; then - status_response=$(curl -s "${server_url}/session/status") -fi - -# Close each session +# Process each session closed_count=0 +skipped_count=0 failed_count=0 results=() -echo "$sessions_to_close" | jq -r '.[]' | while read -r sid; do +echo "$sessions_to_close" | jq -c '.[]' | while read -r session; do + sid=$(echo "$session" | jq -r '.id') + title=$(echo "$session" | jq -r '.title') + # Check if we should close this session should_close=true - if [ "$only_completed" != "false" ]; then - status=$(echo "$status_response" | jq -r --arg id "$sid" '.[$id] // "unknown"') + if [ "$force" != "true" ] && [ "$only_completed" != "false" ]; then + status=$(get_session_status "$sid" "$server_url") if [ "$status" != "completed" ] && [ "$status" != "error" ] && [ "$status" != "aborted" ]; then should_close=false + results+=("{\"session_id\": \"$sid\", \"title\": \"$title\", \"status\": \"skipped\", \"reason\": \"still_$status\"}") + ((skipped_count++)) fi fi if [ "$should_close" = "true" ]; then # Delete the session - delete_response=$(curl -s -w "\n%{http_code}" -X DELETE "${server_url}/session/${sid}") + delete_response=$(retry_with_backoff \ + "curl -s -w '\n%{http_code}' -X DELETE '${server_url}/session/${sid}'" \ + "delete session $sid") + http_code=$(echo "$delete_response" | tail -n1) if [ "$http_code" = "200" ] || [ "$http_code" = "204" ]; then + results+=("{\"session_id\": \"$sid\", \"title\": \"$title\", \"status\": \"closed\"}") ((closed_count++)) - results+=("{\"session_id\": \"$sid\", \"status\": \"closed\"}") else + results+=("{\"session_id\": \"$sid\", \"title\": \"$title\", \"status\": \"failed\", \"http_code\": \"$http_code\"}") ((failed_count++)) - results+=("{\"session_id\": \"$sid\", \"status\": \"failed\", \"http_code\": \"$http_code\"}") fi - else - results+=("{\"session_id\": \"$sid\", \"status\": \"skipped\", \"reason\": \"still_running\"}") fi done -# Return summary -echo "{ - \"closed\": $closed_count, - \"failed\": $failed_count, - \"results\": $(printf '%s\n' "${results[@]}" | jq -s '.') -}" +# Save results +printf '%s\n' "${results[@]}" | jq -s '.' > /tmp/cleanup_results.json + +# Format summary +summary=$(cat </dev/null) + last_message=$(retry_with_backoff \ + "curl -s '${server_url}/session/${sid}/message?limit=1'" \ + "get last message for $sid" || echo "[]") - if [ -n "$last_message_time" ]; then - last_activity=$(date -d "$last_message_time" +%s 2>/dev/null || echo "0") - idle_seconds=$((current_time - last_activity)) - - if [ $idle_seconds -gt $idle_threshold ] && [ "$status" = "running" ]; then - health="idle" + last_message_time=$(echo "$last_message" | jq -r '.[0].info.timestamp // empty' 2>/dev/null) + + health="unknown" + idle_minutes="null" + + if [ -n "$last_message_time" ] && [ "$last_message_time" != "null" ]; then + # Convert timestamp to seconds (handle both ms and s timestamps) + if [ ${#last_message_time} -gt 10 ]; then + last_activity=$((last_message_time / 1000)) else - health="healthy" + last_activity=$last_message_time fi + idle_seconds=$((current_time - last_activity)) idle_minutes=$((idle_seconds / 60)) - echo "{ - \"session_id\": \"$sid\", - \"title\": \"$title\", - \"status\": \"$status\", - \"health\": \"$health\", - \"idle_minutes\": $idle_minutes, - \"last_activity\": \"$last_message_time\" - }" - else - echo "{ - \"session_id\": \"$sid\", - \"title\": \"$title\", - \"status\": \"$status\", - \"health\": \"unknown\", - \"idle_minutes\": null, - \"last_activity\": null - }" + + # Determine health based on status and activity + if [ "$status" = "completed" ] || [ "$status" = "error" ] || [ "$status" = "aborted" ]; then + health="finished" + elif [ "$status" = "running" ] || [ "$status" = "busy" ]; then + if [ $idle_seconds -gt $idle_threshold ]; then + health="stuck" # Running but no activity + else + health="healthy" + fi + elif [ "$status" = "initializing" ]; then + if [ $idle_seconds -gt 300 ]; then # 5 minutes to initialize + health="stuck" + else + health="healthy" + fi + else + health="idle" + fi + elif [ "$status" = "completed" ] || [ "$status" = "error" ]; then + health="finished" fi -done | jq -s '.' + + health_results+=("{ + \"session_id\": \"$sid\", + \"title\": \"$title\", + \"status\": \"$status\", + \"health\": \"$health\", + \"idle_minutes\": $idle_minutes, + \"last_activity\": \"$last_message_time\" + }") +done + +# Format results +printf '%s\n' "${health_results[@]}" | jq -s '.' > /tmp/health_results.json +all_results=$(cat /tmp/health_results.json) +rm -f /tmp/health_results.json + +# Count health statuses +healthy_count=$(echo "$all_results" | jq '[.[] | select(.health == "healthy")] | length') +stuck_count=$(echo "$all_results" | jq '[.[] | select(.health == "stuck")] | length') +idle_count=$(echo "$all_results" | jq '[.[] | select(.health == "idle")] | length') +finished_count=$(echo "$all_results" | jq '[.[] | select(.health == "finished")] | length') + +summary="Health Summary: +- Healthy: $healthy_count sessions +- Stuck: $stuck_count sessions (running but inactive > ${idle_threshold_minutes}min) +- Idle: $idle_count sessions +- Finished: $finished_count sessions" + +if [ $stuck_count -gt 0 ]; then + format_response "warning" "Health monitoring" "$all_results" "$summary + +Stuck sessions may need attention: +- Check if they're waiting for input +- Consider restarting if truly stuck" +else + format_response "success" "Health monitoring" "$all_results" "$summary" +fi ``` ## Usage Examples @@ -377,35 +881,37 @@ result = task( ) ``` -### Checking session status: +### Advanced search: ```python result = task( subagent_type="async-agent-manager", - prompt="Get status of all sessions with tag pattern AUTO-IMP" + prompt='''Search for sessions with these criteria: + - search_pattern: tag:AUTO-IMP*,status:running,age:>30m + - combine_filters: AND''' ) ``` -### Getting session messages: +### Getting all messages (default): ```python result = task( subagent_type="async-agent-manager", - prompt="Get the last 10 messages from session abc123-def456" + prompt="Get messages from session abc123-def456" ) ``` -### Monitoring health: +### Getting paginated messages: ```python result = task( subagent_type="async-agent-manager", - prompt="Check health of all AUTO-IMP sessions, marking as idle if no activity for 10 minutes" + prompt="Get messages from session abc123-def456 with limit=50 and offset=100" ) ``` -### Cleaning up: +### Force cleanup: ```python result = task( subagent_type="async-agent-manager", - prompt="Close all completed sessions with tag pattern AUTO-IMP" + prompt="Close all sessions with tag pattern AUTO-TEST with force=true" ) ``` @@ -435,20 +941,20 @@ Common tag prefixes used in CleverAgents: - `AUTO-TIME` - Timeline updater supervisor - `AUTO-OWNR` - Project owner supervisor - `AUTO-WDOG` - System watchdog supervisor +- `AUTO-ONEOFF` - One-off fix agents ## Error Handling -All operations return structured JSON responses: -- Success: `{"status": "success", ...additional fields...}` -- Error: `{"status": "error", "reason": "...", ...details...}` -- Skipped: `{"status": "skipped", "reason": "...", ...details...}` - -Always check the status field before processing results. +All operations use: +1. **Retry logic** with exponential backoff (5 attempts: 0s, 5s, 30s, 2m, 5m) +2. **Structured responses** with consistent formatting +3. **Helpful suggestions** for resolving issues +4. **Graceful degradation** when APIs are unavailable ## Security Notes - This agent is the ONLY one with permission to curl to localhost:4096 - All other agents must use this agent via the Task tool - Properly escapes all JSON to prevent injection -- Validates all inputs before making API calls +- Validates agent names before launching (with force override) - Never exposes raw API errors to calling agents \ No newline at end of file