Skip to content

Commit 359762c

Browse files
authored
fix: on ctrl-c for claude mode, set status to complete (#1226)
# Motivation <!-- Why is this change necessary? --> # Content <!-- Please include a summary of the change --> # Testing <!-- How was the change tested? --> # Please check the following before marking your PR as ready for review - [ ] I have added tests for my changes - [ ] I have updated the documentation or added new documentation as needed
1 parent 9d24402 commit 359762c

File tree

2 files changed

+66
-95
lines changed

2 files changed

+66
-95
lines changed

src/codegen/cli/commands/claude/claude_log_watcher.py

Lines changed: 64 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,16 @@
77

88
from .quiet_console import console
99

10-
from .claude_log_utils import (
11-
get_claude_session_log_path,
12-
parse_jsonl_line,
13-
read_existing_log_lines,
14-
validate_log_entry,
15-
format_log_for_api
16-
)
10+
from .claude_log_utils import get_claude_session_log_path, parse_jsonl_line, read_existing_log_lines, validate_log_entry, format_log_for_api
1711
from .claude_session_api import send_claude_session_log
1812

1913

2014
class ClaudeLogWatcher:
2115
"""Watches Claude Code session log files for new entries and sends them to the API."""
22-
23-
def __init__(
24-
self,
25-
session_id: str,
26-
org_id: Optional[int] = None,
27-
poll_interval: float = 1.0,
28-
on_log_entry: Optional[Callable[[Dict[str, Any]], None]] = None
29-
):
16+
17+
def __init__(self, session_id: str, org_id: Optional[int] = None, poll_interval: float = 1.0, on_log_entry: Optional[Callable[[Dict[str, Any]], None]] = None):
3018
"""Initialize the log watcher.
31-
19+
3220
Args:
3321
session_id: The Claude session ID to watch
3422
org_id: Organization ID for API calls
@@ -39,56 +27,56 @@ def __init__(
3927
self.org_id = org_id
4028
self.poll_interval = poll_interval
4129
self.on_log_entry = on_log_entry
42-
30+
4331
self.log_path = get_claude_session_log_path(session_id)
4432
self.last_line_count = 0
4533
self.is_running = False
4634
self.watcher_thread: Optional[threading.Thread] = None
47-
35+
4836
# Stats
4937
self.total_entries_processed = 0
5038
self.total_entries_sent = 0
5139
self.total_send_failures = 0
52-
40+
5341
def start(self) -> bool:
5442
"""Start the log watcher in a background thread.
55-
43+
5644
Returns:
5745
True if started successfully, False otherwise
5846
"""
5947
if self.is_running:
6048
console.print(f"⚠️ Log watcher for session {self.session_id[:8]}... is already running", style="yellow")
6149
return False
62-
50+
6351
# Initialize line count
6452
self.last_line_count = read_existing_log_lines(self.log_path)
65-
53+
6654
self.is_running = True
6755
self.watcher_thread = threading.Thread(target=self._watch_loop, daemon=True)
6856
self.watcher_thread.start()
69-
57+
7058
console.print(f"📋 Started log watcher for session {self.session_id[:8]}...", style="green")
7159
console.print(f" Log file: {self.log_path}", style="dim")
7260
console.print(f" Starting from line: {self.last_line_count + 1}", style="dim")
73-
61+
7462
return True
75-
63+
7664
def stop(self) -> None:
7765
"""Stop the log watcher."""
7866
if not self.is_running:
7967
return
80-
68+
8169
self.is_running = False
82-
70+
8371
if self.watcher_thread and self.watcher_thread.is_alive():
8472
self.watcher_thread.join(timeout=2.0)
85-
73+
8674
console.print(f"📋 Stopped log watcher for session {self.session_id[:8]}...", style="dim")
8775
console.print(f" Processed: {self.total_entries_processed} entries", style="dim")
8876
console.print(f" Sent: {self.total_entries_sent} entries", style="dim")
8977
if self.total_send_failures > 0:
9078
console.print(f" Failures: {self.total_send_failures} entries", style="yellow")
91-
79+
9280
def _watch_loop(self) -> None:
9381
"""Main watching loop that runs in a background thread."""
9482
while self.is_running:
@@ -98,104 +86,104 @@ def _watch_loop(self) -> None:
9886
except Exception as e:
9987
console.print(f"⚠️ Error in log watcher: {e}", style="yellow")
10088
time.sleep(self.poll_interval * 2) # Back off on errors
101-
89+
10290
def _check_for_new_entries(self) -> None:
10391
"""Check for new log entries and process them."""
10492
if not self.log_path.exists():
10593
return
106-
94+
10795
try:
10896
current_line_count = read_existing_log_lines(self.log_path)
109-
97+
11098
if current_line_count > self.last_line_count:
11199
new_entries = self._read_new_lines(self.last_line_count, current_line_count)
112-
100+
113101
for entry in new_entries:
114102
self._process_log_entry(entry)
115-
103+
116104
self.last_line_count = current_line_count
117-
105+
118106
except Exception as e:
119107
console.print(f"⚠️ Error reading log file: {e}", style="yellow")
120-
108+
121109
def _read_new_lines(self, start_line: int, end_line: int) -> list[Dict[str, Any]]:
122110
"""Read new lines from the log file.
123-
111+
124112
Args:
125113
start_line: Line number to start from (0-indexed)
126114
end_line: Line number to end at (0-indexed, exclusive)
127-
115+
128116
Returns:
129117
List of parsed log entries
130118
"""
131119
entries = []
132-
120+
133121
try:
134-
with open(self.log_path, 'r', encoding='utf-8') as f:
122+
with open(self.log_path, "r", encoding="utf-8") as f:
135123
lines = f.readlines()
136-
124+
137125
# Read only the new lines
138126
for i in range(start_line, min(end_line, len(lines))):
139127
line = lines[i]
140128
entry = parse_jsonl_line(line)
141-
129+
142130
if entry is not None:
143131
entries.append(entry)
144-
132+
145133
except (OSError, UnicodeDecodeError) as e:
146134
console.print(f"⚠️ Error reading log file: {e}", style="yellow")
147-
135+
148136
return entries
149-
137+
150138
def _process_log_entry(self, log_entry: Dict[str, Any]) -> None:
151139
"""Process a single log entry.
152-
140+
153141
Args:
154142
log_entry: The parsed log entry
155143
"""
156144
self.total_entries_processed += 1
157-
145+
158146
# Validate the entry
159147
if not validate_log_entry(log_entry):
160148
console.print(f"⚠️ Invalid log entry skipped: {log_entry}", style="yellow")
161149
return
162-
150+
163151
# Format for API
164152
formatted_entry = format_log_for_api(log_entry)
165-
153+
166154
# Call optional callback
167155
if self.on_log_entry:
168156
try:
169157
self.on_log_entry(formatted_entry)
170158
except Exception as e:
171159
console.print(f"⚠️ Error in log entry callback: {e}", style="yellow")
172-
160+
173161
# Send to API
174162
self._send_log_entry(formatted_entry)
175-
163+
176164
def _send_log_entry(self, log_entry: Dict[str, Any]) -> None:
177165
"""Send a log entry to the API.
178-
166+
179167
Args:
180168
log_entry: The formatted log entry
181169
"""
182170
try:
183171
success = send_claude_session_log(self.session_id, log_entry, self.org_id)
184-
172+
185173
if success:
186174
self.total_entries_sent += 1
187175
# Only show verbose output in debug mode
188176
console.print(f"📤 Sent log entry: {log_entry.get('type', 'unknown')}", style="dim")
189177
else:
190178
self.total_send_failures += 1
191-
179+
192180
except Exception as e:
193181
self.total_send_failures += 1
194182
console.print(f"⚠️ Failed to send log entry: {e}", style="yellow")
195-
183+
196184
def get_stats(self) -> Dict[str, Any]:
197185
"""Get watcher statistics.
198-
186+
199187
Returns:
200188
Dictionary with watcher stats
201189
"""
@@ -208,96 +196,79 @@ def get_stats(self) -> Dict[str, Any]:
208196
"total_entries_processed": self.total_entries_processed,
209197
"total_entries_sent": self.total_entries_sent,
210198
"total_send_failures": self.total_send_failures,
211-
"success_rate": (
212-
self.total_entries_sent / max(1, self.total_entries_processed) * 100
213-
if self.total_entries_processed > 0 else 0
214-
)
199+
"success_rate": (self.total_entries_sent / max(1, self.total_entries_processed) * 100 if self.total_entries_processed > 0 else 0),
215200
}
216201

217202

218203
class ClaudeLogWatcherManager:
219204
"""Manages multiple log watchers for different sessions."""
220-
205+
221206
def __init__(self):
222207
self.watchers: Dict[str, ClaudeLogWatcher] = {}
223-
224-
def start_watcher(
225-
self,
226-
session_id: str,
227-
org_id: Optional[int] = None,
228-
poll_interval: float = 1.0,
229-
on_log_entry: Optional[Callable[[Dict[str, Any]], None]] = None
230-
) -> bool:
208+
209+
def start_watcher(self, session_id: str, org_id: Optional[int] = None, poll_interval: float = 1.0, on_log_entry: Optional[Callable[[Dict[str, Any]], None]] = None) -> bool:
231210
"""Start a log watcher for a session.
232-
211+
233212
Args:
234213
session_id: The Claude session ID
235214
org_id: Organization ID for API calls
236215
poll_interval: How often to check for new entries (seconds)
237216
on_log_entry: Optional callback for each new log entry
238-
217+
239218
Returns:
240219
True if started successfully, False otherwise
241220
"""
242221
if session_id in self.watchers:
243222
console.print(f"⚠️ Watcher for session {session_id[:8]}... already exists", style="yellow")
244223
return False
245-
246-
watcher = ClaudeLogWatcher(
247-
session_id=session_id,
248-
org_id=org_id,
249-
poll_interval=poll_interval,
250-
on_log_entry=on_log_entry
251-
)
252-
224+
225+
watcher = ClaudeLogWatcher(session_id=session_id, org_id=org_id, poll_interval=poll_interval, on_log_entry=on_log_entry)
226+
253227
if watcher.start():
254228
self.watchers[session_id] = watcher
255229
return True
256230
return False
257-
231+
258232
def stop_watcher(self, session_id: str) -> None:
259233
"""Stop a log watcher for a session.
260-
234+
261235
Args:
262236
session_id: The Claude session ID
263237
"""
264238
if session_id in self.watchers:
265239
self.watchers[session_id].stop()
266240
del self.watchers[session_id]
267-
241+
268242
def stop_all_watchers(self) -> None:
269243
"""Stop all active watchers."""
270244
for session_id in list(self.watchers.keys()):
271245
self.stop_watcher(session_id)
272-
246+
273247
def get_active_sessions(self) -> list[str]:
274248
"""Get list of active session IDs being watched.
275-
249+
276250
Returns:
277251
List of session IDs
278252
"""
279253
return list(self.watchers.keys())
280-
254+
281255
def get_watcher_stats(self, session_id: str) -> Optional[Dict[str, Any]]:
282256
"""Get stats for a specific watcher.
283-
257+
284258
Args:
285259
session_id: The Claude session ID
286-
260+
287261
Returns:
288262
Watcher stats or None if not found
289263
"""
290264
if session_id in self.watchers:
291265
return self.watchers[session_id].get_stats()
292266
return None
293-
267+
294268
def get_all_stats(self) -> Dict[str, Dict[str, Any]]:
295269
"""Get stats for all active watchers.
296-
270+
297271
Returns:
298272
Dictionary mapping session IDs to their stats
299273
"""
300-
return {
301-
session_id: watcher.get_stats()
302-
for session_id, watcher in self.watchers.items()
303-
}
274+
return {session_id: watcher.get_stats() for session_id, watcher in self.watchers.items()}

src/codegen/cli/commands/claude/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def signal_handler(signum, frame):
174174
process.terminate()
175175
cleanup_claude_hook()
176176
cleanup_codegen_mcp_server()
177-
update_claude_session_status(session_id, "ERROR", resolved_org_id)
177+
update_claude_session_status(session_id, "COMPLETE", resolved_org_id)
178178
sys.exit(0)
179179

180180
signal.signal(signal.SIGINT, signal_handler)
@@ -200,7 +200,7 @@ def signal_handler(signum, frame):
200200
except KeyboardInterrupt:
201201
console.print("\n🛑 Interrupted by user", style="yellow")
202202
log_watcher_manager.stop_all_watchers()
203-
update_claude_session_status(session_id, "ERROR", resolved_org_id)
203+
update_claude_session_status(session_id, "CANCELLED", resolved_org_id)
204204
except Exception as e:
205205
console.print(f"❌ Error running Claude Code: {e}", style="red")
206206
log_watcher_manager.stop_all_watchers()

0 commit comments

Comments
 (0)