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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions