@@ -52,6 +52,9 @@ detect_output_format() {
5252
5353# Parse JSON response and extract structured fields
5454# Creates .json_parse_result with normalized analysis data
55+ # Supports TWO JSON formats:
56+ # 1. Flat format: { status, exit_signal, work_type, files_modified, ... }
57+ # 2. Claude CLI format: { result, sessionId, metadata: { files_changed, has_errors, completion_status, ... } }
5558parse_json_response () {
5659 local output_file=$1
5760 local result_file=" ${2:- .json_parse_result} "
@@ -67,22 +70,57 @@ parse_json_response() {
6770 return 1
6871 fi
6972
70- # Extract fields with defaults
73+ # Detect JSON format by checking for Claude CLI fields
74+ local has_result_field=$( jq -r ' has("result")' " $output_file " 2> /dev/null)
75+
76+ # Extract fields - support both flat format and Claude CLI format
77+ # Priority: Claude CLI fields first, then flat format fields
78+
79+ # Status: from flat format OR derived from metadata.completion_status
7180 local status=$( jq -r ' .status // "UNKNOWN"' " $output_file " 2> /dev/null)
81+ local completion_status=$( jq -r ' .metadata.completion_status // ""' " $output_file " 2> /dev/null)
82+ if [[ " $completion_status " == " complete" || " $completion_status " == " COMPLETE" ]]; then
83+ status=" COMPLETE"
84+ fi
85+
86+ # Exit signal: from flat format OR derived from completion_status
7287 local exit_signal=$( jq -r ' .exit_signal // false' " $output_file " 2> /dev/null)
88+
89+ # Work type: from flat format
7390 local work_type=$( jq -r ' .work_type // "UNKNOWN"' " $output_file " 2> /dev/null)
74- local files_modified=$( jq -r ' .files_modified // 0' " $output_file " 2> /dev/null)
91+
92+ # Files modified: from flat format OR from metadata.files_changed
93+ local files_modified=$( jq -r ' .metadata.files_changed // .files_modified // 0' " $output_file " 2> /dev/null)
94+
95+ # Error count: from flat format OR derived from metadata.has_errors
96+ # Note: When only has_errors=true is present (without explicit error_count),
97+ # we set error_count=1 as a minimum. This is defensive programming since
98+ # the stuck detection threshold is >5 errors, so 1 error won't trigger it.
99+ # Actual error count may be higher, but precise count isn't critical for our logic.
75100 local error_count=$( jq -r ' .error_count // 0' " $output_file " 2> /dev/null)
76- local summary=$( jq -r ' .summary // ""' " $output_file " 2> /dev/null)
101+ local has_errors=$( jq -r ' .metadata.has_errors // false' " $output_file " 2> /dev/null)
102+ if [[ " $has_errors " == " true" && " $error_count " == " 0" ]]; then
103+ error_count=1 # At least one error if has_errors is true
104+ fi
77105
78- # Extract nested metadata if present
106+ # Summary: from flat format OR from result field (Claude CLI format)
107+ local summary=$( jq -r ' .result // .summary // ""' " $output_file " 2> /dev/null)
108+
109+ # Session ID: from Claude CLI format (sessionId) OR from metadata.session_id
110+ local session_id=$( jq -r ' .sessionId // .metadata.session_id // ""' " $output_file " 2> /dev/null)
111+
112+ # Loop number: from metadata
79113 local loop_number=$( jq -r ' .metadata.loop_number // .loop_number // 0' " $output_file " 2> /dev/null)
80- local session_id=$( jq -r ' .metadata.session_id // ""' " $output_file " 2> /dev/null)
114+
115+ # Confidence: from flat format
81116 local confidence=$( jq -r ' .confidence // 0' " $output_file " 2> /dev/null)
82117
118+ # Progress indicators: from Claude CLI metadata (optional)
119+ local progress_count=$( jq -r ' .metadata.progress_indicators | if . then length else 0 end' " $output_file " 2> /dev/null)
120+
83121 # Normalize values
84122 # Convert exit_signal to boolean string
85- if [[ " $exit_signal " == " true" || " $status " == " COMPLETE" ]]; then
123+ if [[ " $exit_signal " == " true" || " $status " == " COMPLETE" || " $completion_status " == " complete " || " $completion_status " == " COMPLETE " ]]; then
86124 exit_signal=" true"
87125 else
88126 exit_signal=" false"
@@ -94,7 +132,7 @@ parse_json_response() {
94132 is_test_only=" true"
95133 fi
96134
97- # Determine is_stuck from error_count
135+ # Determine is_stuck from error_count (threshold >5)
98136 local is_stuck=" false"
99137 error_count=$(( error_count + 0 )) # Ensure integer
100138 if [[ $error_count -gt 5 ]]; then
@@ -104,12 +142,23 @@ parse_json_response() {
104142 # Ensure files_modified is integer
105143 files_modified=$(( files_modified + 0 ))
106144
145+ # Ensure progress_count is integer
146+ progress_count=$(( progress_count + 0 ))
147+
107148 # Calculate has_completion_signal
108149 local has_completion_signal=" false"
109150 if [[ " $status " == " COMPLETE" || " $exit_signal " == " true" ]]; then
110151 has_completion_signal=" true"
111152 fi
112153
154+ # Boost confidence based on structured data availability
155+ if [[ " $has_result_field " == " true" ]]; then
156+ confidence=$(( confidence + 20 )) # Structured response boost
157+ fi
158+ if [[ $progress_count -gt 0 ]]; then
159+ confidence=$(( confidence + progress_count * 5 )) # Progress indicators boost
160+ fi
161+
113162 # Write normalized result using jq for safe JSON construction
114163 # String fields use --arg (auto-escapes), numeric/boolean use --argjson
115164 jq -n \
@@ -184,6 +233,13 @@ analyze_response() {
184233 work_summary=$( jq -r ' .summary' .json_parse_result 2> /dev/null || echo " " )
185234 files_modified=$( jq -r ' .files_modified' .json_parse_result 2> /dev/null || echo " 0" )
186235 local json_confidence=$( jq -r ' .confidence' .json_parse_result 2> /dev/null || echo " 0" )
236+ local session_id=$( jq -r ' .session_id' .json_parse_result 2> /dev/null || echo " " )
237+
238+ # Persist session ID if present (for session continuity across loop iterations)
239+ if [[ -n " $session_id " && " $session_id " != " null" ]]; then
240+ store_session_id " $session_id "
241+ [[ " ${VERBOSE_PROGRESS:- } " == " true" ]] && echo " DEBUG: Persisted session ID: $session_id " >&2
242+ fi
187243
188244 # JSON parsing provides high confidence
189245 if [[ " $exit_signal " == " true" ]]; then
@@ -511,10 +567,117 @@ detect_stuck_loop() {
511567 fi
512568}
513569
570+ # =============================================================================
571+ # SESSION MANAGEMENT FUNCTIONS
572+ # =============================================================================
573+
574+ # Session file location - standardized across ralph_loop.sh and response_analyzer.sh
575+ SESSION_FILE=" .claude_session_id"
576+ # Session expiration time in seconds (24 hours)
577+ SESSION_EXPIRATION_SECONDS=86400
578+
579+ # Store session ID to file with timestamp
580+ # Usage: store_session_id "session-uuid-123"
581+ store_session_id () {
582+ local session_id=$1
583+
584+ if [[ -z " $session_id " ]]; then
585+ return 1
586+ fi
587+
588+ # Write session with timestamp using jq for safe JSON construction
589+ jq -n \
590+ --arg session_id " $session_id " \
591+ --arg timestamp " $( get_iso_timestamp) " \
592+ ' {
593+ session_id: $session_id,
594+ timestamp: $timestamp
595+ }' > " $SESSION_FILE "
596+
597+ return 0
598+ }
599+
600+ # Get the last stored session ID
601+ # Returns: session ID string or empty if not found
602+ get_last_session_id () {
603+ if [[ ! -f " $SESSION_FILE " ]]; then
604+ echo " "
605+ return 0
606+ fi
607+
608+ # Extract session_id from JSON file
609+ local session_id=$( jq -r ' .session_id // ""' " $SESSION_FILE " 2> /dev/null)
610+ echo " $session_id "
611+ return 0
612+ }
613+
614+ # Check if the stored session should be resumed
615+ # Returns: 0 (true) if session is valid and recent, 1 (false) otherwise
616+ should_resume_session () {
617+ if [[ ! -f " $SESSION_FILE " ]]; then
618+ echo " false"
619+ return 1
620+ fi
621+
622+ # Get session timestamp
623+ local timestamp=$( jq -r ' .timestamp // ""' " $SESSION_FILE " 2> /dev/null)
624+
625+ if [[ -z " $timestamp " ]]; then
626+ echo " false"
627+ return 1
628+ fi
629+
630+ # Calculate session age using date utilities
631+ local now=$( get_epoch_seconds)
632+ local session_time
633+
634+ # Parse ISO timestamp to epoch - try multiple formats for cross-platform compatibility
635+ # Strip milliseconds if present (e.g., 2026-01-09T10:30:00.123+00:00 → 2026-01-09T10:30:00+00:00)
636+ local clean_timestamp=" ${timestamp} "
637+ if [[ " $timestamp " =~ \. [0-9]+[+-Z] ]]; then
638+ clean_timestamp=$( echo " $timestamp " | sed ' s/\.[0-9]*\([+-Z]\)/\1/' )
639+ fi
640+
641+ if command -v gdate & > /dev/null; then
642+ # macOS with coreutils
643+ session_time=$( gdate -d " $clean_timestamp " +%s 2> /dev/null)
644+ elif date --version 2>&1 | grep -q GNU; then
645+ # GNU date (Linux)
646+ session_time=$( date -d " $clean_timestamp " +%s 2> /dev/null)
647+ else
648+ # BSD date (macOS without coreutils) - try parsing ISO format
649+ # Format: 2026-01-09T10:30:00+00:00 or 2026-01-09T10:30:00Z
650+ # Strip timezone suffix for BSD date parsing
651+ local date_only=" ${clean_timestamp% [+-Z]* } "
652+ session_time=$( date -j -f " %Y-%m-%dT%H:%M:%S" " $date_only " +%s 2> /dev/null)
653+ fi
654+
655+ # If we couldn't parse the timestamp, consider session expired
656+ if [[ -z " $session_time " || ! " $session_time " =~ ^[0-9]+$ ]]; then
657+ echo " false"
658+ return 1
659+ fi
660+
661+ # Calculate age in seconds
662+ local age=$(( now - session_time))
663+
664+ # Check if session is still valid (less than expiration time)
665+ if [[ $age -lt $SESSION_EXPIRATION_SECONDS ]]; then
666+ echo " true"
667+ return 0
668+ else
669+ echo " false"
670+ return 1
671+ fi
672+ }
673+
514674# Export functions for use in ralph_loop.sh
515675export -f detect_output_format
516676export -f parse_json_response
517677export -f analyze_response
518678export -f update_exit_signals
519679export -f log_analysis_summary
520680export -f detect_stuck_loop
681+ export -f store_session_id
682+ export -f get_last_session_id
683+ export -f should_resume_session
0 commit comments