fix(backend/chat): Resolve symlinks in session file path for --resume

The CLI resolves symlinks when computing its project directory (e.g.
/tmp -> /private/tmp on macOS), so our session file writes must use
the resolved path to match. Also adds cwd to ClaudeAgentOptions and
debug logging for SDK messages.
This commit is contained in:
Zamil Majdy
2026-02-10 20:11:16 +04:00
parent 51aa369c80
commit acb2d0bd1b
3 changed files with 63 additions and 5 deletions

View File

@@ -287,6 +287,7 @@ async def stream_chat_completion_sdk(
mcp_servers={"copilot": mcp_server}, # type: ignore[arg-type]
allowed_tools=COPILOT_TOOL_NAMES,
hooks=create_security_hooks(user_id), # type: ignore[arg-type]
cwd="/tmp",
resume=resume_id,
)
@@ -326,6 +327,9 @@ async def stream_chat_completion_sdk(
# Receive messages from the SDK
async for sdk_msg in client.receive_messages():
logger.debug(
f"[SDK] Received: {type(sdk_msg).__name__} {getattr(sdk_msg, 'subtype', '')}"
)
for response in adapter.convert_message(sdk_msg):
if isinstance(response, StreamStart):
continue

View File

@@ -26,8 +26,13 @@ def _encode_cwd(cwd: str) -> str:
def _get_project_dir(cwd: str) -> Path:
"""Get the CLI project directory for a given working directory."""
return _CLAUDE_PROJECTS_DIR / _encode_cwd(cwd)
"""Get the CLI project directory for a given working directory.
Resolves symlinks to match the CLI's behavior (e.g. /tmp -> /private/tmp
on macOS).
"""
resolved = str(Path(cwd).resolve())
return _CLAUDE_PROJECTS_DIR / _encode_cwd(resolved)
def write_session_file(
@@ -45,6 +50,7 @@ def write_session_file(
return None
session_id = session.session_id
resolved_cwd = str(Path(cwd).resolve())
project_dir = _get_project_dir(cwd)
project_dir.mkdir(parents=True, exist_ok=True)
@@ -62,7 +68,7 @@ def write_session_file(
"parentUuid": prev_uuid,
"isSidechain": False,
"userType": "external",
"cwd": cwd,
"cwd": resolved_cwd,
"sessionId": session_id,
"type": "user",
"message": {"role": "user", "content": msg.content},
@@ -77,7 +83,7 @@ def write_session_file(
"parentUuid": prev_uuid,
"isSidechain": False,
"userType": "external",
"cwd": cwd,
"cwd": resolved_cwd,
"sessionId": session_id,
"type": "assistant",
"message": {

View File

@@ -6,7 +6,7 @@ from pathlib import Path
from unittest.mock import patch
from ..model import ChatMessage, ChatSession
from .session_file import cleanup_session_file, write_session_file
from .session_file import _get_project_dir, cleanup_session_file, write_session_file
_NOW = datetime.now(UTC)
@@ -172,3 +172,51 @@ def test_cleanup_no_error_if_missing(tmp_path: Path):
return_value=tmp_path,
):
cleanup_session_file("nonexistent") # Should not raise
# -- _get_project_dir --------------------------------------------------------
def test_get_project_dir_resolves_symlinks(tmp_path: Path):
"""_get_project_dir should resolve symlinks so the path matches the CLI."""
# Create a symlink: tmp_path/link -> tmp_path/real
real_dir = tmp_path / "real"
real_dir.mkdir()
link = tmp_path / "link"
link.symlink_to(real_dir)
with patch(
"backend.api.features.chat.sdk.session_file._CLAUDE_PROJECTS_DIR",
tmp_path / "projects",
):
result = _get_project_dir(str(link))
# Should resolve the symlink and encode the real path
expected_encoded = "-" + str(real_dir).lstrip("/").replace("/", "-")
assert result.name == expected_encoded
def test_write_uses_resolved_cwd_in_messages(tmp_path: Path):
"""The cwd field in JSONL messages should use the resolved path."""
session = _make_session(
[
ChatMessage(role="user", content="hello"),
ChatMessage(role="assistant", content="Hi!"),
ChatMessage(role="user", content="current"),
],
session_id="sess-cwd",
)
with patch(
"backend.api.features.chat.sdk.session_file._get_project_dir",
return_value=tmp_path,
):
write_session_file(session, cwd="/tmp")
file_path = tmp_path / "sess-cwd.jsonl"
lines = file_path.read_text().strip().split("\n")
for line in lines:
obj = json.loads(line)
# On macOS /tmp resolves to /private/tmp; on Linux stays /tmp
resolved = str(Path("/tmp").resolve())
assert obj["cwd"] == resolved