fix(agent): use --resume on existing sessions, --session-id only for new
Claude Code rejects --session-id on a session that already exists on disk
('Session ID ... is already in use'). The first message of a conversation
must use --session-id to create; every message after must use --resume.
Detect by checking ~/.claude/projects/<encoded-cwd>/<uuid>.jsonl. Plus a
belt-and-suspenders retry: if --session-id surprisingly fails with the
'already in use' string, automatically retry with --resume.
This was the bug that caused chat windows to fail on the second message.
This commit is contained in:
parent
0745aefdb9
commit
6d5819d297
1 changed files with 62 additions and 6 deletions
|
|
@ -44,8 +44,23 @@ class ClaudeError(RuntimeError):
|
||||||
"""Raised when the claude CLI returns a non-zero exit or unparseable output."""
|
"""Raised when the claude CLI returns a non-zero exit or unparseable output."""
|
||||||
|
|
||||||
|
|
||||||
|
def _session_exists(session_id: str) -> bool:
|
||||||
|
"""True if Claude Code has already persisted a JSONL for this session.
|
||||||
|
|
||||||
|
Claude Code stores sessions at ~/.claude/projects/<encoded-cwd>/<uuid>.jsonl
|
||||||
|
where non-alphanumerics in the cwd are replaced with hyphens.
|
||||||
|
"""
|
||||||
|
encoded = "".join(c if c.isalnum() else "-" for c in CLAUDE_CWD)
|
||||||
|
path = Path.home() / ".claude" / "projects" / encoded / f"{session_id}.jsonl"
|
||||||
|
return path.is_file()
|
||||||
|
|
||||||
|
|
||||||
async def ask_claude(message: str, session_id: str) -> ClaudeResult:
|
async def ask_claude(message: str, session_id: str) -> ClaudeResult:
|
||||||
"""Send `message` to `claude -p` resuming session_id; return parsed result.
|
"""Send `message` to `claude -p` for `session_id`; return parsed result.
|
||||||
|
|
||||||
|
On the FIRST message of a session uses `--session-id <uuid>` to create it.
|
||||||
|
On subsequent messages uses `--resume <uuid>` because claude rejects
|
||||||
|
`--session-id` on existing sessions ("Session ID ... is already in use").
|
||||||
|
|
||||||
Raises ClaudeError on subprocess failure, JSON parse failure, or timeout.
|
Raises ClaudeError on subprocess failure, JSON parse failure, or timeout.
|
||||||
"""
|
"""
|
||||||
|
|
@ -75,10 +90,15 @@ async def ask_claude(message: str, session_id: str) -> ClaudeResult:
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Pick --session-id (creates) vs --resume (continues) based on whether
|
||||||
|
# the session JSONL already exists on disk.
|
||||||
|
is_new = not _session_exists(session_id)
|
||||||
|
session_flag = "--session-id" if is_new else "--resume"
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
CLAUDE_BIN,
|
CLAUDE_BIN,
|
||||||
"-p",
|
"-p",
|
||||||
"--session-id",
|
session_flag,
|
||||||
session_id,
|
session_id,
|
||||||
"--output-format",
|
"--output-format",
|
||||||
"json",
|
"json",
|
||||||
|
|
@ -90,7 +110,11 @@ async def ask_claude(message: str, session_id: str) -> ClaudeResult:
|
||||||
]
|
]
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"claude exec: session=%s msg_len=%d cwd=%s", session_id, len(message), CLAUDE_CWD
|
"claude exec: session=%s mode=%s msg_len=%d cwd=%s",
|
||||||
|
session_id,
|
||||||
|
"new" if is_new else "resume",
|
||||||
|
len(message),
|
||||||
|
CLAUDE_CWD,
|
||||||
)
|
)
|
||||||
|
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
|
@ -114,8 +138,40 @@ async def ask_claude(message: str, session_id: str) -> ClaudeResult:
|
||||||
raise ClaudeError(f"claude timed out after {CLAUDE_TIMEOUT_S}s")
|
raise ClaudeError(f"claude timed out after {CLAUDE_TIMEOUT_S}s")
|
||||||
|
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
|
stderr_text = stderr.decode("utf-8", "replace")
|
||||||
|
# If we picked the wrong flag (e.g. JSONL deleted from disk between
|
||||||
|
# our check and exec, or a never-flushed session), claude prints
|
||||||
|
# "Session ID … is already in use." Re-issue with --resume.
|
||||||
|
if is_new and "already in use" in stderr_text:
|
||||||
|
logger.info("session %s actually exists; retrying with --resume", session_id)
|
||||||
|
args2 = list(args)
|
||||||
|
args2[2] = "--resume"
|
||||||
|
proc2 = await asyncio.create_subprocess_exec(
|
||||||
|
*args2,
|
||||||
|
stdin=asyncio.subprocess.PIPE,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
cwd=CLAUDE_CWD,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
stdout, stderr = await asyncio.wait_for(
|
||||||
|
proc2.communicate(input=message.encode("utf-8")),
|
||||||
|
timeout=CLAUDE_TIMEOUT_S,
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
try:
|
||||||
|
proc2.kill()
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
raise ClaudeError(f"claude timed out after {CLAUDE_TIMEOUT_S}s")
|
||||||
|
if proc2.returncode != 0:
|
||||||
raise ClaudeError(
|
raise ClaudeError(
|
||||||
f"claude exited {proc.returncode}: {stderr.decode('utf-8', 'replace')[:500]}"
|
f"claude exited {proc2.returncode} after retry: "
|
||||||
|
f"{stderr.decode('utf-8', 'replace')[:500]}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ClaudeError(
|
||||||
|
f"claude exited {proc.returncode}: {stderr_text[:500]}"
|
||||||
)
|
)
|
||||||
|
|
||||||
raw_text = stdout.decode("utf-8", "replace").strip()
|
raw_text = stdout.decode("utf-8", "replace").strip()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue