diff --git a/CLAUDE.md b/CLAUDE.md index 7560f96..418161e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,7 +36,7 @@ The system uses a modular architecture with reusable components in the `lib/` di - Supports both flat JSON format and Claude CLI format (`result`, `sessionId`, `metadata`) - Extracts structured fields: status, exit_signal, work_type, files_modified - **Session management**: `store_session_id()`, `get_last_session_id()`, `should_resume_session()` - - Automatic session persistence to `.session_id` file with 24-hour expiration + - Automatic session persistence to `.claude_session_id` file with 24-hour expiration - Detects test-only loops and stuck error patterns - Two-stage error filtering to eliminate false positives - Multi-line error matching for accurate stuck loop detection @@ -314,7 +314,7 @@ bats tests/unit/test_cli_parsing.bats - `get_last_session_id()` - Retrieves stored session ID - `should_resume_session()` - Checks session validity (24-hour expiration) - Added `get_epoch_seconds()` to date_utils.sh for cross-platform epoch time -- Auto-persists sessionId to `.session_id` file during response analysis +- Auto-persists sessionId to `.claude_session_id` file during response analysis - Added 16 new tests covering Claude CLI format and session management - Test count: 239 (up from 223) diff --git a/lib/response_analyzer.sh b/lib/response_analyzer.sh index c3c138c..107326d 100644 --- a/lib/response_analyzer.sh +++ b/lib/response_analyzer.sh @@ -72,7 +72,6 @@ parse_json_response() { # Detect JSON format by checking for Claude CLI fields local has_result_field=$(jq -r 'has("result")' "$output_file" 2>/dev/null) - local has_session_id_field=$(jq -r 'has("sessionId")' "$output_file" 2>/dev/null) # Extract fields - support both flat format and Claude CLI format # Priority: Claude CLI fields first, then flat format fields @@ -94,6 +93,10 @@ parse_json_response() { local files_modified=$(jq -r '.metadata.files_changed // .files_modified // 0' "$output_file" 2>/dev/null) # Error count: from flat format OR derived from metadata.has_errors + # Note: When only has_errors=true is present (without explicit error_count), + # we set error_count=1 as a minimum. This is defensive programming since + # the stuck detection threshold is >5 errors, so 1 error won't trigger it. + # Actual error count may be higher, but precise count isn't critical for our logic. local error_count=$(jq -r '.error_count // 0' "$output_file" 2>/dev/null) local has_errors=$(jq -r '.metadata.has_errors // false' "$output_file" 2>/dev/null) if [[ "$has_errors" == "true" && "$error_count" == "0" ]]; then @@ -232,9 +235,10 @@ analyze_response() { local json_confidence=$(jq -r '.confidence' .json_parse_result 2>/dev/null || echo "0") local session_id=$(jq -r '.session_id' .json_parse_result 2>/dev/null || echo "") - # Persist session ID if present + # Persist session ID if present (for session continuity across loop iterations) if [[ -n "$session_id" && "$session_id" != "null" ]]; then store_session_id "$session_id" + [[ "${VERBOSE_PROGRESS:-}" == "true" ]] && echo "DEBUG: Persisted session ID: $session_id" >&2 fi # JSON parsing provides high confidence @@ -567,8 +571,8 @@ detect_stuck_loop() { # SESSION MANAGEMENT FUNCTIONS # ============================================================================= -# Session file location -SESSION_FILE=".session_id" +# Session file location - standardized across ralph_loop.sh and response_analyzer.sh +SESSION_FILE=".claude_session_id" # Session expiration time in seconds (24 hours) SESSION_EXPIRATION_SECONDS=86400 @@ -628,16 +632,24 @@ should_resume_session() { local session_time # Parse ISO timestamp to epoch - try multiple formats for cross-platform compatibility + # Strip milliseconds if present (e.g., 2026-01-09T10:30:00.123+00:00 → 2026-01-09T10:30:00+00:00) + local clean_timestamp="${timestamp}" + if [[ "$timestamp" =~ \.[0-9]+[+-Z] ]]; then + clean_timestamp=$(echo "$timestamp" | sed 's/\.[0-9]*\([+-Z]\)/\1/') + fi + if command -v gdate &>/dev/null; then # macOS with coreutils - session_time=$(gdate -d "$timestamp" +%s 2>/dev/null) + session_time=$(gdate -d "$clean_timestamp" +%s 2>/dev/null) elif date --version 2>&1 | grep -q GNU; then # GNU date (Linux) - session_time=$(date -d "$timestamp" +%s 2>/dev/null) + session_time=$(date -d "$clean_timestamp" +%s 2>/dev/null) else # BSD date (macOS without coreutils) - try parsing ISO format - # Format: 2026-01-09T10:30:00+00:00 or similar - session_time=$(date -j -f "%Y-%m-%dT%H:%M:%S" "${timestamp%[+-]*}" +%s 2>/dev/null) + # Format: 2026-01-09T10:30:00+00:00 or 2026-01-09T10:30:00Z + # Strip timezone suffix for BSD date parsing + local date_only="${clean_timestamp%[+-Z]*}" + session_time=$(date -j -f "%Y-%m-%dT%H:%M:%S" "$date_only" +%s 2>/dev/null) fi # If we couldn't parse the timestamp, consider session expired diff --git a/tests/unit/test_json_parsing.bats b/tests/unit/test_json_parsing.bats index e304bb4..e29c94e 100644 --- a/tests/unit/test_json_parsing.bats +++ b/tests/unit/test_json_parsing.bats @@ -657,7 +657,7 @@ EOF assert_equal "$output_format" "json" } -@test "analyze_response persists sessionId to .session_id file" { +@test "analyze_response persists sessionId to .claude_session_id file" { local output_file="$LOG_DIR/test_output.log" cat > "$output_file" << 'EOF' @@ -670,9 +670,9 @@ EOF analyze_response "$output_file" 1 # Session ID should be persisted for continuity - [[ -f ".session_id" ]] || skip "Session persistence not yet implemented" + [[ -f ".claude_session_id" ]] || skip "Session persistence not yet implemented" - local stored_session=$(cat .session_id) + local stored_session=$(cat .claude_session_id) [[ "$stored_session" == *"session-persist-test-123"* ]] } @@ -683,15 +683,15 @@ EOF @test "store_session_id writes session to file with timestamp" { run store_session_id "session-test-abc" - [[ -f ".session_id" ]] || skip "store_session_id not yet implemented" + [[ -f ".claude_session_id" ]] || skip "store_session_id not yet implemented" - local content=$(cat .session_id) + local content=$(cat .claude_session_id) [[ "$content" == *"session-test-abc"* ]] } @test "get_last_session_id retrieves stored session" { # First store a session - echo '{"session_id": "session-retrieve-test", "timestamp": "2026-01-09T10:00:00Z"}' > .session_id + echo '{"session_id": "session-retrieve-test", "timestamp": "2026-01-09T10:00:00Z"}' > .claude_session_id run get_last_session_id @@ -699,7 +699,7 @@ EOF } @test "get_last_session_id returns empty when no session file" { - rm -f .session_id + rm -f .claude_session_id run get_last_session_id @@ -711,7 +711,7 @@ EOF @test "should_resume_session returns true for recent session" { # Store a recent session (simulated as current timestamp) local now=$(date +%s) - echo "{\"session_id\": \"session-recent\", \"timestamp\": \"$(date -Iseconds)\"}" > .session_id + echo "{\"session_id\": \"session-recent\", \"timestamp\": \"$(date -Iseconds)\"}" > .claude_session_id run should_resume_session @@ -721,7 +721,7 @@ EOF @test "should_resume_session returns false for old session" { # Store an old session (24+ hours ago) - echo '{"session_id": "session-old", "timestamp": "2020-01-01T00:00:00Z"}' > .session_id + echo '{"session_id": "session-old", "timestamp": "2020-01-01T00:00:00Z"}' > .claude_session_id run should_resume_session @@ -730,7 +730,7 @@ EOF } @test "should_resume_session returns false when no session file" { - rm -f .session_id + rm -f .claude_session_id run should_resume_session