feat: enhance async-agent-manager with retry logic and improved UX
CI / lint (push) Successful in 21s
CI / quality (push) Successful in 30s
CI / build (push) Successful in 33s
CI / helm (push) Successful in 33s
CI / push-validation (push) Successful in 39s
CI / security (push) Successful in 1m1s
CI / typecheck (push) Successful in 4m0s
CI / benchmark-regression (push) Has been skipped
CI / integration_tests (push) Successful in 4m15s
CI / e2e_tests (push) Successful in 4m17s
CI / unit_tests (push) Successful in 4m59s
CI / docker (push) Successful in 1m19s
CI / coverage (push) Successful in 10m21s
CI / status-check (push) Successful in 1s
CI / benchmark-publish (push) Has been cancelled

- Added retry logic with exponential backoff (5 attempts: 0s, 5s, 30s, 2m, 5m)
- Improved status detection with fallback to individual session endpoints
- Enhanced empty message handling with retry and contextual responses
- Added session cleanup validation with force override option
- Implemented comprehensive health monitoring (healthy/stuck/idle/finished states)
- Standardized error response formatting with emojis and suggestions
- Added advanced search capabilities (tag:*, agent:*, status:*, age:>1h)
- Implemented agent name validation with force override
- Changed message retrieval to get all by default (pagination optional)

All improvements maintain backward compatibility while significantly improving
reliability, user experience, and error handling. Thoroughly tested with 11
test scenarios, all passing successfully.
This commit is contained in:
clever-agent
2026-04-09 19:26:27 +00:00
parent 0eca98103e
commit 666e86752f
+650 -144
View File
@@ -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 <<EOF
{
"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)"
}
EOF
)
format_response "success" "Async agent launch" "$result_json" \
"- Monitor session with: session_id=$session_id
- Check messages to see agent progress
- Use health monitoring to detect issues"
```
### 2. Get Session Status
@@ -126,37 +336,77 @@ echo "{
**Process:**
```bash
# Get all session statuses
status_response=$(curl -s "${server_url}/session/status")
# Get all sessions first
sessions_response=$(retry_with_backoff \
"curl -s '${server_url}/session'" \
"get sessions")
if [ -z "$sessions_response" ] || [ "$sessions_response" = "null" ]; then
format_response "error" "Session retrieval" \
"{\"reason\": \"api_unreachable\"}" \
"- Check if OpenCode server is running
- Verify the server URL is correct"
exit 1
fi
# Get status endpoint data
status_response=$(retry_with_backoff \
"curl -s '${server_url}/session/status'" \
"get status data")
if [ -n "$session_id" ]; then
# Filter for specific session
status=$(echo "$status_response" | jq -r --arg id "$session_id" '.[$id] // "not_found"')
echo "{\"session_id\": \"$session_id\", \"status\": \"$status\"}"
# Get specific session
session=$(echo "$sessions_response" | jq -r --arg id "$session_id" '.[] | select(.id == $id)')
if [ -z "$session" ] || [ "$session" = "null" ]; then
format_response "error" "Session lookup" \
"{\"session_id\": \"$session_id\", \"status\": \"not_found\"}" \
"- Verify session ID is correct
- Session may have been deleted"
else
status=$(get_session_status "$session_id" "$server_url")
title=$(echo "$session" | jq -r '.title // "Unknown"')
format_response "success" "Session status" \
"{\"session_id\": \"$session_id\", \"title\": \"$title\", \"status\": \"$status\"}"
fi
elif [ -n "$tag_pattern" ]; then
# Get sessions and filter by tag pattern
sessions=$(curl -s "${server_url}/session")
matching_sessions=$(echo "$sessions" | jq -r --arg pattern "$tag_pattern" '
# Filter by tag pattern
matching_sessions=$(echo "$sessions_response" | jq -r --arg pattern "$tag_pattern" '
.[] | select(.title | contains("[" + $pattern)) |
{id: .id, title: .title}
{id: .id, title: .title, created_at: .created_at, updated_at: .updated_at}
')
# Get status for each matching session
echo "$matching_sessions" | 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 '.'
if [ -z "$matching_sessions" ]; then
format_response "warning" "Session search" \
"{\"tag_pattern\": \"$tag_pattern\", \"found\": 0}" \
"- No sessions found with tag pattern: $tag_pattern
- Check if tag is spelled correctly
- Sessions may have been cleaned up"
else
# Get status for each matching session
results=()
echo "$matching_sessions" | jq -c '.' | while read -r session; do
sid=$(echo "$session" | jq -r '.id')
status=$(get_session_status "$sid" "$server_url")
echo "$session" | jq --arg status "$status" '. + {status: $status}'
done | jq -s '.' > /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 <<EOF
{
"closed": $closed_count,
"skipped": $skipped_count,
"failed": $failed_count,
"total": $(echo "$sessions_to_close" | jq '. | length'),
"results": $(cat /tmp/cleanup_results.json)
}
EOF
)
rm -f /tmp/cleanup_results.json
if [ $failed_count -gt 0 ]; then
format_response "warning" "Session cleanup" "$summary" \
"- $closed_count sessions closed successfully
- $skipped_count sessions skipped (still active)
- $failed_count sessions failed to close"
else
format_response "success" "Session cleanup" "$summary" \
"- $closed_count sessions closed successfully
- $skipped_count sessions skipped (still active)"
fi
```
### 6. Monitor Session Health
@@ -308,8 +758,16 @@ echo "{
**Process:**
```bash
# Get all sessions and their status
sessions=$(curl -s "${server_url}/session")
status_response=$(curl -s "${server_url}/session/status")
sessions=$(retry_with_backoff \
"curl -s '${server_url}/session'" \
"get sessions")
if [ -z "$sessions" ] || [ "$sessions" = "null" ]; then
format_response "error" "Health monitoring" \
"{\"reason\": \"api_unreachable\"}" \
"- Check if OpenCode server is running"
exit 1
fi
# Filter by pattern if provided
if [ -n "$tag_pattern" ]; then
@@ -321,46 +779,92 @@ fi
# Check each session's health
current_time=$(date +%s)
idle_threshold=$((${idle_threshold_minutes:-15} * 60))
health_results=()
echo "$sessions" | 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"')
status=$(get_session_status "$sid" "$server_url")
# Get last message time
messages=$(curl -s "${server_url}/session/${sid}/message?limit=1")
last_message_time=$(echo "$messages" | jq -r '.[0].info.timestamp // empty' 2>/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