Skip to content

Commit 3091101

Browse files
committed
Fix session resume using correct project working directory
- Use session.working_dir instead of config.safe_working_dir when resuming - Optimize session list to limit to 50 most recent for performance - Add recoverable error support for failed session resumes
1 parent 653c72f commit 3091101

3 files changed

Lines changed: 76 additions & 71 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "darkcode-server"
7-
version = "0.1.50"
7+
version = "0.1.51"
88
description = "Server for DarkCode Agent - Remote Claude Code from your phone"
99
readme = "README.md"
1010
license = {text = "MIT"}

src/darkcode_server.egg-info/PKG-INFO

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Metadata-Version: 2.4
22
Name: darkcode-server
3-
Version: 0.1.49
3+
Version: 0.1.50
44
Summary: Server for DarkCode Agent - Remote Claude Code from your phone
55
Author: 0xdeadbeef
66
License: MIT

src/darkcode_server/server.py

Lines changed: 74 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -227,102 +227,103 @@ def get_claude_version() -> str:
227227
return "unknown"
228228

229229

230-
def list_claude_sessions(working_dir: Path) -> list[dict]:
231-
"""List all available Claude Code sessions across all projects.
230+
def list_claude_sessions(working_dir: Path, limit: int = 50) -> list[dict]:
231+
"""List available Claude Code sessions across all projects.
232232
233-
Scans all project directories in ~/.claude/projects/ to find sessions.
233+
Scans project directories in ~/.claude/projects/ to find sessions.
234234
Returns sessions sorted by last modified time (most recent first).
235+
Limited to most recent sessions for performance.
236+
237+
Args:
238+
working_dir: The working directory (used for context)
239+
limit: Maximum number of sessions to return (default 50)
235240
"""
236241
claude_dir = Path.home() / ".claude" / "projects"
237242
if not claude_dir.exists():
238243
return []
239244

240-
sessions = []
245+
# First, collect all session files with their mod times (fast - just stat)
246+
session_files = []
241247

242-
# Scan ALL project directories, not just the working_dir one
243248
for project_dir in claude_dir.iterdir():
244249
if not project_dir.is_dir():
245250
continue
246251

247-
# The directory name is the encoded path (e.g., -Users-0xdeadbeef-hakc-dev)
248252
encoded_path = project_dir.name
249-
250-
# Decode to actual filesystem path for cd'ing into
251253
actual_path = _decode_claude_project_path(encoded_path)
252254

253255
for session_file in project_dir.glob("*.jsonl"):
254256
# Skip agent sessions (subagent files)
255257
if session_file.stem.startswith("agent-"):
256258
continue
257259

258-
# Skip empty files
259260
try:
260261
stat = session_file.stat()
261262
if stat.st_size == 0:
262263
continue
264+
session_files.append((session_file, stat, project_dir, actual_path))
263265
except (IOError, OSError):
264266
continue
265267

266-
try:
267-
# Try to find actual user text for preview
268-
preview = ""
269-
session_id = session_file.stem
270-
271-
# Check if there's a subdirectory with same name (active session)
272-
has_subdir = (project_dir / session_id).is_dir()
273-
274-
# Scan through lines to find real user messages (not IDE context)
275-
with open(session_file, "r") as f:
276-
lines_checked = 0
277-
for line in f:
278-
lines_checked += 1
279-
if lines_checked > 100: # Don't scan forever
280-
break
281-
try:
282-
data = json.loads(line)
283-
if data.get("type") == "user":
284-
msg = data.get("message", {})
285-
content = msg.get("content", [])
286-
if isinstance(content, list) and content:
287-
for block in content:
288-
if isinstance(block, dict) and block.get("type") == "text":
289-
text = block.get("text", "").strip()
290-
# Skip IDE/system messages that start with <
291-
if not text.startswith("<") and len(text) > 5:
292-
# Clean up: take first line, truncate
293-
first_line = text.split("\n")[0][:120]
294-
preview = first_line
295-
break
296-
if preview:
297-
break
298-
except json.JSONDecodeError:
299-
continue
268+
# Sort by modification time (newest first) and take only the limit
269+
session_files.sort(key=lambda x: x[1].st_mtime, reverse=True)
270+
session_files = session_files[:limit]
300271

301-
# Count messages for sync detection
302-
message_count = 0
303-
try:
304-
with open(session_file, "r") as f:
305-
for line in f:
306-
try:
307-
data = json.loads(line)
308-
if data.get("type") in ("user", "assistant"):
309-
message_count += 1
310-
except json.JSONDecodeError:
311-
continue
312-
except (IOError, OSError):
313-
pass
272+
sessions = []
314273

315-
sessions.append({
316-
"sessionId": session_id,
317-
"lastModified": int(stat.st_mtime * 1000),
318-
"size": stat.st_size,
319-
"preview": preview,
320-
"isActive": has_subdir,
321-
"projectPath": actual_path,
322-
"messageCount": message_count, # For Whisper Sync
323-
})
324-
except (IOError, OSError):
325-
continue
274+
for session_file, stat, project_dir, actual_path in session_files:
275+
try:
276+
preview = ""
277+
message_count = 0
278+
session_id = session_file.stem
279+
280+
# Check if there's a subdirectory with same name (active session)
281+
has_subdir = (project_dir / session_id).is_dir()
282+
283+
# Single pass through file: find preview and count messages
284+
with open(session_file, "r") as f:
285+
lines_checked = 0
286+
for line in f:
287+
lines_checked += 1
288+
# Limit lines scanned for preview, but count all messages
289+
if lines_checked > 200:
290+
# After 200 lines, just count without parsing
291+
message_count += line.count('"type": "user"') + line.count('"type": "assistant"')
292+
continue
293+
294+
try:
295+
data = json.loads(line)
296+
msg_type = data.get("type")
297+
298+
# Count user/assistant messages
299+
if msg_type in ("user", "assistant"):
300+
message_count += 1
301+
302+
# Find preview (first real user message)
303+
if not preview and msg_type == "user":
304+
msg = data.get("message", {})
305+
content = msg.get("content", [])
306+
if isinstance(content, list) and content:
307+
for block in content:
308+
if isinstance(block, dict) and block.get("type") == "text":
309+
text = block.get("text", "").strip()
310+
if not text.startswith("<") and len(text) > 5:
311+
preview = text.split("\n")[0][:120]
312+
break
313+
except json.JSONDecodeError:
314+
continue
315+
316+
sessions.append({
317+
"sessionId": session_id,
318+
"lastModified": int(stat.st_mtime * 1000),
319+
"size": stat.st_size,
320+
"preview": preview,
321+
"isActive": has_subdir,
322+
"projectPath": actual_path,
323+
"messageCount": message_count, # For Whisper Sync
324+
})
325+
except (IOError, OSError):
326+
continue
326327

327328
# Sort: active sessions first, then by last modified (most recent first)
328329
sessions.sort(key=lambda x: (not x["isActive"], -x["lastModified"]))
@@ -883,8 +884,12 @@ async def _start_claude_process(self, session: Session, resume_session_id: str =
883884
resume_session_id: Optional Claude session ID to resume (for handoffs)
884885
"""
885886
try:
886-
# Use validated working directory
887-
working_dir = self.config.safe_working_dir
887+
# Use session's working directory if set (e.g., when resuming a session),
888+
# otherwise fall back to config's validated working directory
889+
if session.working_dir and session.working_dir.exists():
890+
working_dir = session.working_dir
891+
else:
892+
working_dir = self.config.safe_working_dir
888893

889894
# Build command with streaming and permission handling
890895
cmd = [

0 commit comments

Comments
 (0)