-
Notifications
You must be signed in to change notification settings - Fork 7
t1398.4: Add session count awareness #2883
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As in
Suggested change
References
|
||||||
| 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 | ||||||
|
|
||||||
| 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) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using
Suggested change
References
|
||||||
|
|
||||||
| 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() { | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The References
|
||||||
| 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 "$@" | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suppressing stderr with
2>/dev/nullcan hide important diagnostic messages from the helper script, such as syntax errors or problems with dependencies likepgrep. The project's general rules advise against blanket error suppression to make debugging easier. The|| trueis sufficient to prevent the script from exiting on a warning (exit code 1).References