Skip to content

Detection Reference

How agentwatch detects programs and states.

Overview

agentwatch uses pattern matching on terminal content to detect:

  1. Program - Which program is running (Claude Code, Codex, shell)
  2. State - What the program is doing (Idle, Working, Blocked, Error)

Each detection produces a confidence score (0.0 to 1.0). Detections with confidence > 0.5 are used.

Detection Pipeline

Terminal Capture → ANSI Strip → Program Detection → State Detection → Result
  1. Capture: Raw terminal content from tmux
  2. Strip: Remove ANSI escape codes for analysis
  3. Program: Match patterns to identify the program
  4. State: Use program-specific patterns for state
  5. Result: Program, state, detail, and confidence scores

Programs

Claude Code

Detected by:

Pattern Confidence Description
Version header 1.0 Claude Code vX.X.X in header
Trust dialog 0.95 "Do you trust the files" prompt
Prompt with hints 0.9 prompt with "? for shortcuts"
Welcome message 0.85 "Welcome back" with prompt
Just prompt 0.6 character alone

Codex

Detected by header patterns and UI elements specific to Codex.

Shell (bash/zsh)

Detected by shell prompt patterns:

Pattern Description
user@host:path$ Standard bash prompt
$ at line end Generic shell prompt
% at line end zsh default prompt

Shell sessions are always considered Idle.

States

Idle

The program is waiting for user input.

Detection Patterns:

Pattern Weight Description
Autosuggest visible 100 ❯ text ↵ - strongest idle signal
Completion message 60 "Brewed for 3m 14s" etc.
Welcome screen 50 Fresh start, no conversation
Shortcuts hint 45 "? for shortcuts" visible
Prompt at bottom 40 near bottom of screen
Little content after prompt 25 ≤3 lines after prompt
Has prompt 20 visible anywhere

How it works:

Patterns are weighted and summed. If idle score > working score, state is Idle.

Working

The program is actively processing.

Detection Patterns:

Pattern Weight Description
Compacting conversation 95 "Compacting conversation…"
Esc to interrupt + thinking 95 Active thinking indicator
Esc to interrupt 90 Status line during processing
Running indicator 90 "Running…" visible
Ctrl+b background hint 85 Command actively running
Queued messages 80 User typed while processing
Running in background 75 Background task active
Activity status line 65 ✶ Doing… with timing
Tool execution 55 Bash ───── tool boxes
Spinner animation 50 Braille spinners (⠋⠙⠹)
Streaming response 35 Lots of content after prompt

Spinner Characters:

⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✽✳✢✶✻·

These indicate active processing when visible.

Blocked

The program needs user action to continue.

Detection Patterns:

Pattern Weight Description
Trust dialog 100 "Do you trust the files"
Navigation hint 95 "↑/↓ to navigate"
Esc to cancel 95 Dismissible dialog
Proceed prompt 90 "Do you want to proceed?"
Type to search 90 Fuzzy finder active
Menu selection 85 ❯ 1. Option list
Enter to select 85 Selection required
Allow permission 80 "Allow X?" prompt
Yes/No confirmation 30 Generic y/n prompt

Detail Values:

Detail Meaning
Permission dialog Needs y/n approval
Menu selection Choose from options
Trust dialog Workspace trust prompt
Search picker Fuzzy finder open

Error

Something went wrong.

Detection Patterns:

Pattern Weight Description
Hung state 90 >5 min "working" but prompt visible
Error keyword 70 Error: at line start
Error with message 65 Error: <message>
API error 65 "API request error"
ERROR in caps 60 All-caps ERROR
Rate limit 60 Rate limiting messages
Crash 60 "crash" or "crashed"
Failed to 55 "failed to X"
Timeout 55 "timed out"
Connection error 55 "connection refused" etc.
Exception 50 "exception" keyword

Hung Detection:

If the prompt is at the bottom but status shows >5 minutes of working with 0 tokens consumed, this indicates a stuck state (Claude Code bug).

Weight System

State detection uses weighted scoring:

  1. Match all patterns against terminal content
  2. Sum weights for each state (Idle, Working, Blocked, Error)
  3. Highest score wins

Example:

Terminal shows:
- "❯" prompt (Idle: +20)
- "? for shortcuts" (Idle: +45)
- Near bottom of screen (Idle: +40)
- No spinner visible

Idle total: 105
Working total: 0
Blocked total: 0

Result: Idle (confidence based on margin)

Confidence Calculation

Confidence reflects how certain the detection is:

  • 1.0: Definitive match (e.g., version header)
  • 0.9+: Very high confidence
  • 0.7-0.9: High confidence
  • 0.5-0.7: Moderate confidence
  • <0.5: Detection rejected

Activity Markers

These characters appear before status messages:

✶✽⏺·✳✢✻

Used in patterns like:

  • ✶ Doing…
  • · Thinking…
  • ✻ Brewed for 3m 14s

Box Drawing Characters

UI chrome characters (ignored in content analysis):

─━│┃┄┅┆┇┈┉┊┋╌╍╎╏═║╒╓╔╕╖╗╘╙╚╛╜╝╞╟╠╡╢╣╤╥╦╧╨╩╪╫╬╭╮╯╰╱╲╳

Lines containing only these are skipped when analyzing content.

Debugging Detection

Enable Debug Logging

service:
  debug:
    enabled: true
    capture_dir: ~/.cache/agentwatch/captures
    log_state_changes: true
    log_detections: true

State Change Markers

State changes are logged with markers for easy grep:

*** STATE CHANGE #123 *** Working -> Idle (detail: Ready)

View Captures

# Find state change in logs
grep "STATE CHANGE" ~/.cache/agentwatch/daemon.log

# View the capture that triggered it
cat ~/.cache/agentwatch/captures/session-name/capture_*.txt

Known Issues

Phantom Working State

Claude Code bug: Sometimes shows working indicators (spinner, status line) but is actually idle. Detected by:

  • Prompt visible at bottom
  • Status shows >5 minutes working
  • Zero tokens consumed ("↓ 0 tokens")

agentwatch detects this with high weight (300) to override false working signals.

Stale Status Lines

After Claude finishes, status lines may remain visible while the prompt scrolls into view. The weight system handles this by giving stronger weight to idle indicators like the prompt position.

Extending Detection

Custom Detectors

Detectors extend BaseDetector and implement:

class MyDetector(BaseDetector):
    def detect_program(self, text: str) -> DetectionResult:
        # Return program name and confidence
        pass

    def detect_state(self, text: str, program: str) -> DetectionResult:
        # Return state and detail
        pass

Adding Patterns

Patterns are defined in agentwatch/detectors/patterns.py:

IDLE_PATTERNS.append(Pattern(
    name="my_pattern",
    weight=50,
    reason="Why this indicates idle",
    contains="my idle indicator"
))