Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .agents/configs/aidevops-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@
"default": "haiku",
"enum": ["haiku", "flash", "sonnet", "pro", "opus"],
"description": "Model tier used for verification checks."
},
"max_interactive_sessions": {
"type": "integer",
"default": 5,
"minimum": 0,
"description": "Maximum concurrent interactive AI sessions before warning. 0 disables. Env: AIDEVOPS_MAX_SESSIONS"
}
},
"additionalProperties": false
Expand Down
8 changes: 7 additions & 1 deletion .agents/configs/aidevops.defaults.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,13 @@
"verification_enabled": true,

// Model tier used for verification checks (cheapest sufficient tier).
"verification_tier": "haiku"
"verification_tier": "haiku",

// Maximum number of concurrent interactive AI sessions before warning.
// Counts processes matching known AI coding assistants (opencode, claude, etc.).
// Set to 0 to disable the session count check.
// Env override: AIDEVOPS_MAX_SESSIONS
"max_interactive_sessions": 5
},

// ---------------------------------------------------------------------------
Expand Down
10 changes: 10 additions & 0 deletions .agents/scripts/aidevops-update-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -247,13 +247,23 @@ main() {
echo "$nudge_output"
fi

# Check for excessive concurrent interactive sessions (t1398.4)
local session_warning=""
if [[ -x "${script_dir}/session-count-helper.sh" ]]; then
session_warning="$("${script_dir}/session-count-helper.sh" check 2>/dev/null || true)"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Suppressing stderr with 2>/dev/null can hide important diagnostic messages from the helper script, such as syntax errors or problems with dependencies like pgrep. The project's general rules advise against blanket error suppression to make debugging easier. The || true is sufficient to prevent the script from exiting on a warning (exit code 1).

Suggested change
session_warning="$("${script_dir}/session-count-helper.sh" check 2>/dev/null || true)"
session_warning="$("${script_dir}/session-count-helper.sh" check || true)"
References
  1. Avoid blanket suppression of command errors with '2>/dev/null' in shell scripts to ensure that system issues remain visible for debugging.

fi
if [[ -n "$session_warning" ]]; then
echo "$session_warning"
fi

# Cache output for agents without Bash (e.g., Plan+)
local cache_dir="$HOME/.aidevops/cache"
mkdir -p "$cache_dir"
{
echo "$output"
[[ -n "$runtime_hint" ]] && echo "$runtime_hint"
[[ -n "$nudge_output" ]] && echo "$nudge_output"
[[ -n "$session_warning" ]] && echo "$session_warning"
} >"$cache_dir/session-greeting.txt"

return 0
Expand Down
8 changes: 8 additions & 0 deletions .agents/scripts/pre-edit-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,14 @@ else
run_operation_verification "$VERIFY_OP"
fi

# Session count warning (t1398.4) — non-blocking, informational only
if [[ -x "$SCRIPT_DIR/session-count-helper.sh" ]]; then
session_warning=$("$SCRIPT_DIR/session-count-helper.sh" check 2>/dev/null || true)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

As in aidevops-update-check.sh, suppressing stderr with 2>/dev/null here can hide important errors from the helper script. Please remove it to allow diagnostic messages to be visible, in line with the project's general rules for debugging.

Suggested change
session_warning=$("$SCRIPT_DIR/session-count-helper.sh" check 2>/dev/null || true)
session_warning=$("$SCRIPT_DIR/session-count-helper.sh" check || true)
References
  1. Avoid blanket suppression of command errors with '2>/dev/null' in shell scripts to ensure that system issues remain visible for debugging.

if [[ -n "$session_warning" ]]; then
echo -e "${YELLOW}${session_warning}${NC}"
fi
fi

echo -e "${GREEN}OK${NC} - On branch: ${BOLD}$current_branch${NC} (in worktree)"
exit 0
fi
Expand Down
307 changes: 307 additions & 0 deletions .agents/scripts/session-count-helper.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
#!/usr/bin/env bash
# =============================================================================
# Session Count Helper (t1398.4)
# =============================================================================
# Counts concurrent interactive AI coding sessions and warns when the count
# exceeds a configurable threshold (default: 5).
#
# Interactive sessions are AI coding assistants running in a terminal (TUI),
# as opposed to headless workers dispatched via `opencode run` or `claude -p`.
#
# Usage:
# session-count-helper.sh count # Print session count
# session-count-helper.sh check # Check against threshold, warn if exceeded
# session-count-helper.sh list # List detected sessions with details
# session-count-helper.sh help # Show usage
#
# Configuration:
# Config key: safety.max_interactive_sessions (default: 5, 0 = disabled)
# Env override: AIDEVOPS_MAX_SESSIONS
#
# Exit codes:
# 0 - OK (count within threshold, or check disabled)
# 1 - Warning (count exceeds threshold)
# 2 - Error (invalid usage)
# =============================================================================

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Source shared constants for config_get, colors, logging
# shellcheck source=shared-constants.sh
source "${SCRIPT_DIR}/shared-constants.sh"

# =============================================================================
# Session Detection
# =============================================================================
# Detects interactive AI coding sessions by examining running processes.
# Distinguishes interactive (TUI) sessions from headless workers.
#
# Known AI coding assistants and their process signatures:
# opencode (interactive): .opencode (no "run" argument in cmdline)
# opencode (headless): .opencode run ... (has "run" in cmdline)
# claude (interactive): claude (no "-p" or "--print" in cmdline)
# claude (headless): claude -p ... or claude --print ...
# cursor: Cursor process
# windsurf: Windsurf process
# aider: aider (Python process)

# Get the configured maximum session count.
# Priority: env var > JSONC config > default (5)
get_max_sessions() {
# Environment variable override (highest priority)
if [[ -n "${AIDEVOPS_MAX_SESSIONS:-}" ]]; then
echo "$AIDEVOPS_MAX_SESSIONS"
return 0
fi

# JSONC config system
if type config_get &>/dev/null; then
local val
val=$(config_get "safety.max_interactive_sessions" "5")
echo "$val"
return 0
fi

# Default
echo "5"
return 0
}

# Count interactive AI sessions.
# Returns the count on stdout.
# Uses pgrep + /proc/cmdline (Linux) or ps (macOS) to distinguish
# interactive from headless sessions.
count_interactive_sessions() {
local count=0

# --- OpenCode sessions ---
# Interactive opencode: the .opencode binary running WITHOUT "run" as
# the first argument after the binary name.
# Headless: .opencode run ...
local opencode_pids=""
opencode_pids=$(pgrep -f '\.opencode' 2>/dev/null || true)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using 2>/dev/null here suppresses all stderr output from pgrep, which could hide important errors like 'command not found' if pgrep isn't installed, or other system issues. According to the project's general rules, error suppression should be avoided to aid debugging. The || true is sufficient to handle the case where no processes are found. This applies to all pgrep calls in this script (e.g., lines 120, 143, 148, 153, 167, 204).

Suggested change
opencode_pids=$(pgrep -f '\.opencode' 2>/dev/null || true)
opencode_pids=$(pgrep -f '\.opencode' || true)
References
  1. Avoid blanket suppression of command errors with '2>/dev/null' in shell scripts to ensure that system issues remain visible for debugging.


if [[ -n "$opencode_pids" ]]; then
local pid
while IFS= read -r pid; do
[[ -z "$pid" ]] && continue
local cmdline=""
if [[ -r "/proc/${pid}/cmdline" ]]; then
# Linux: /proc/PID/cmdline has null-separated args
cmdline=$(tr '\0' ' ' <"/proc/${pid}/cmdline" 2>/dev/null || true)
else
# macOS fallback: ps -o args=
cmdline=$(ps -o args= -p "$pid" 2>/dev/null || true)
fi

# Skip if this is a headless worker (has "run" after .opencode)
# Also skip language servers and helper processes
if echo "$cmdline" | grep -qE '\.opencode run '; then
continue
fi
# Skip language servers spawned by opencode
if echo "$cmdline" | grep -qE '(typescript-language-server|eslintServer|vscode-)'; then
continue
fi
# Skip node wrapper processes (the actual .opencode binary is what matters)
if echo "$cmdline" | grep -qE '^node .*/bin/opencode'; then
continue
fi
# This is an interactive opencode session
count=$((count + 1))
done <<<"$opencode_pids"
fi

# --- Claude Code sessions ---
# Interactive claude: the claude binary WITHOUT "-p", "--print", or "run"
local claude_pids=""
claude_pids=$(pgrep -x claude 2>/dev/null || true)

if [[ -n "$claude_pids" ]]; then
local pid
while IFS= read -r pid; do
[[ -z "$pid" ]] && continue
local cmdline=""
if [[ -r "/proc/${pid}/cmdline" ]]; then
cmdline=$(tr '\0' ' ' <"/proc/${pid}/cmdline" 2>/dev/null || true)
else
cmdline=$(ps -o args= -p "$pid" 2>/dev/null || true)
fi

# Skip headless modes
if echo "$cmdline" | grep -qE 'claude (-p|--print|run) '; then
continue
fi
count=$((count + 1))
done <<<"$claude_pids"
fi

# --- Cursor sessions ---
local cursor_count=0
cursor_count=$(pgrep -c -f 'Cursor\.app' 2>/dev/null || true)
count=$((count + cursor_count))

# --- Windsurf sessions ---
local windsurf_count=0
windsurf_count=$(pgrep -c -f 'Windsurf' 2>/dev/null || true)
count=$((count + windsurf_count))

# --- Aider sessions ---
local aider_count=0
aider_count=$(pgrep -c -f 'aider' 2>/dev/null || true)
count=$((count + aider_count))

echo "$count"
return 0
}

# List detected interactive sessions with details.
# Output format: PID | APP | RSS_MB | UPTIME | DIR
list_sessions() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The list_sessions function currently only lists details for 'OpenCode' and 'Claude Code' sessions. However, count_interactive_sessions also counts 'Cursor', 'Windsurf', and 'Aider' sessions. This can lead to confusing output from the list command, where the detailed list is shorter than the reported total. To improve clarity, please extend list_sessions to also find and display details for Cursor, Windsurf, and Aider processes, similar to how it's done for OpenCode and Claude Code.

References
  1. An info/describe function like 'list_sessions' should be comprehensive and include all relevant items, especially if another related function (e.g., 'count_interactive_sessions') counts them, to allow users to inspect their own work and avoid confusing output, aligning with the principle that info/describe functions must include relevant details.

local found=0

# --- OpenCode sessions ---
local opencode_pids=""
opencode_pids=$(pgrep -f '\.opencode' 2>/dev/null || true)

if [[ -n "$opencode_pids" ]]; then
local pid
while IFS= read -r pid; do
[[ -z "$pid" ]] && continue
local cmdline=""
if [[ -r "/proc/${pid}/cmdline" ]]; then
cmdline=$(tr '\0' ' ' <"/proc/${pid}/cmdline" 2>/dev/null || true)
else
cmdline=$(ps -o args= -p "$pid" 2>/dev/null || true)
fi

# Skip headless, language servers, node wrappers
if echo "$cmdline" | grep -qE '\.opencode run '; then
continue
fi
if echo "$cmdline" | grep -qE '(typescript-language-server|eslintServer|vscode-)'; then
continue
fi
if echo "$cmdline" | grep -qE '^node .*/bin/opencode'; then
continue
fi

local rss_mb etime
rss_mb=$(ps -o rss= -p "$pid" 2>/dev/null || echo "0")
rss_mb=$((${rss_mb:-0} / 1024))
etime=$(ps -o etime= -p "$pid" 2>/dev/null || echo "unknown")
etime=$(echo "$etime" | tr -d ' ')

echo " PID ${pid} | OpenCode | ${rss_mb} MB | uptime: ${etime}"
found=$((found + 1))
done <<<"$opencode_pids"
fi

# --- Claude Code sessions ---
local claude_pids=""
claude_pids=$(pgrep -x claude 2>/dev/null || true)

if [[ -n "$claude_pids" ]]; then
local pid
while IFS= read -r pid; do
[[ -z "$pid" ]] && continue
local cmdline=""
if [[ -r "/proc/${pid}/cmdline" ]]; then
cmdline=$(tr '\0' ' ' <"/proc/${pid}/cmdline" 2>/dev/null || true)
else
cmdline=$(ps -o args= -p "$pid" 2>/dev/null || true)
fi

if echo "$cmdline" | grep -qE 'claude (-p|--print|run) '; then
continue
fi

local rss_mb etime
rss_mb=$(ps -o rss= -p "$pid" 2>/dev/null || echo "0")
rss_mb=$((${rss_mb:-0} / 1024))
etime=$(ps -o etime= -p "$pid" 2>/dev/null || echo "unknown")
etime=$(echo "$etime" | tr -d ' ')

echo " PID ${pid} | Claude Code | ${rss_mb} MB | uptime: ${etime}"
found=$((found + 1))
done <<<"$claude_pids"
fi

if [[ "$found" -eq 0 ]]; then
echo " No interactive AI sessions detected"
fi

return 0
}

# Check session count against threshold and output a warning if exceeded.
# Returns 0 if within threshold, 1 if exceeded.
check_sessions() {
local max_sessions
max_sessions=$(get_max_sessions)

# Disabled if max is 0
if [[ "$max_sessions" -eq 0 ]]; then
return 0
fi

local session_count
session_count=$(count_interactive_sessions)

if [[ "$session_count" -gt "$max_sessions" ]]; then
echo "SESSION_WARNING: ${session_count} interactive AI sessions detected (threshold: ${max_sessions}). Consider closing unused sessions to reduce memory pressure (~100-400 MB each)."
return 1
fi

return 0
}

# =============================================================================
# CLI Interface
# =============================================================================

show_help() {
echo "Usage: $(basename "$0") <command>"
echo ""
echo "Commands:"
echo " count Print the number of interactive AI sessions"
echo " check Check against threshold, warn if exceeded (exit 1)"
echo " list List detected sessions with PID, app, RSS, uptime"
echo " help Show this help"
echo ""
echo "Configuration:"
echo " Config key: safety.max_interactive_sessions (default: 5)"
echo " Env override: AIDEVOPS_MAX_SESSIONS (0 = disabled)"
return 0
}

main() {
local command="${1:-check}"

case "$command" in
count)
count_interactive_sessions
;;
check)
check_sessions
;;
list)
echo "Interactive AI sessions:"
list_sessions
echo ""
echo "Total: $(count_interactive_sessions) interactive | Threshold: $(get_max_sessions)"
;;
help | --help | -h)
show_help
;;
*)
print_error "Unknown command: $command"
show_help
return 2
;;
esac
}

main "$@"
1 change: 1 addition & 0 deletions .agents/scripts/shared-constants.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1327,6 +1327,7 @@ _ft_env_map() {
repo_sync) echo "AIDEVOPS_REPO_SYNC" ;;
openclaw_auto_update) echo "AIDEVOPS_OPENCLAW_AUTO_UPDATE" ;;
openclaw_freshness_hours) echo "AIDEVOPS_OPENCLAW_FRESHNESS_HOURS" ;;
max_interactive_sessions) echo "AIDEVOPS_MAX_SESSIONS" ;;
*) echo "" ;;
esac
return 0
Expand Down
Loading