Skip to content

Remote handoff crashes in a retry loop with "No conversation found with session ID" when the tracked session's transcript was never materialized #200

Description

@KolorYan

Summary

When an interactive terminal session started with happier is taken over from the web/mobile app (local → remote handoff), the CLI can pass a Claude session id to resume that has no on-disk transcript. Claude Code then exits with No conversation found with session ID: <uuid> and the remote runner retries the same dead id indefinitely, so the session never becomes usable.

Environment

  • CLI: 0.2.1 (self-hosted / preview channel), also reproducible against dev logic (see Root cause)
  • Self-hosted relay (relay-server:preview), SQLite light flavor
  • macOS, Claude Code 2.1.x

Steps to reproduce

  1. In a terminal, run happier in a project directory (starts a local session).
  2. From the web or mobile app, open that session and send a message (triggers local → remote handoff).
  3. The remote runner starts Claude with resume: <trackedSessionId>, but that id has no transcript on disk.

Expected: the handoff starts (or resumes) a working session.
Actual: Error: Claude Code process exited with code 1, subprocess log shows [ERROR] No conversation found with session ID: <uuid>, and the runner loops "Starting new Claude session… / exited unexpectedly. Waiting for the next message to retry…".

Confirmed the id is a ghost: find ~/.claude/projects -name "<uuid>.jsonl" returns nothing, i.e. Claude never materialized a transcript for it. Standalone claude -p --output-format stream-json --input-format stream-json works fine in the same project, so this is not a Claude-side / stream-json issue.

Root cause

apps/cli/src/backends/claude/remote/sessionStartPlan.ts, resolveClaudeRemoteSessionStartPlan.

On dev, the hasMaterializedSessionTranscript guard nulls startFrom only when the transcript file is absent/empty. The very next branch, when the transcript exists but checkSession fails validation, intentionally "attempts resume anyway":

} else if (!effectiveDeps.checkSession(opts.sessionId, opts.path, opts.transcriptPath)) {
  effectiveDeps.logDebug(
    `[${effectiveDeps.logPrefix}] Session ${opts.sessionId} did not pass transcript validation yet; attempting resume anyway`,
  );
}

That branch forwards an unresumable id to the SDK resume, which is exactly what surfaces No conversation found. (On the preview / 0.2.1 code there is no materialized guard at all — the single if (!checkSession) block always attempts resume — so it is even more exposed.)

The call site comment deliberately avoids "failing closed" to preserve context during fast local↔remote switching, but the current behavior trades a recoverable context loss for a hard crash + infinite retry.

Proposed fix

When checkSession fails, start fresh instead of resuming a session the SDK cannot find (one line — set startFrom = null in that branch). This turns an unrecoverable crash loop into a clean fresh start (at worst, loss of prior context for that one handoff), consistent with the existing hasMaterializedSessionTranscript fallback right above it. Happy to open a PR with the change plus a sessionStartPlan.test.ts case asserting startFrom === null when checkSession returns false.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions