Skip to content

cursor: resume_unavailable when cursorSessionId never persisted (early exit before init) #744

@heavygee

Description

@heavygee

Summary

POST /api/sessions/:id/resume returns HTTP 500 with {"code":"resume_unavailable","error":"Resume session ID unavailable"} when resuming an inactive/archived cursor session whose hub metadata never received cursorSessionId.

This is distinct from #728 (resume/config RPC race after hub restart). Here the resume token was never persisted because the CLI archived before the Cursor system init stream event.

Root cause

syncEngine.resolveLocalResumeTarget() requires metadata.cursorSessionId for cursor flavor (hub/src/sync/syncEngine.ts):

if (flavor === 'cursor') return metadata.cursorSessionId ?? null
// ...
if (!agentSessionId) {
    return { type: 'error', message: 'Resume session ID unavailable', code: 'resume_unavailable' }
}

CursorSession.onSessionFound() persists the ID via updateMetadata, but remote cursor only calls it when the agent stream emits system/init with session_id. If the process exits or is terminated before that event, metadata stays empty even though:

  • The CLI was spawned with --resume <uuid> (ID known at spawn)
  • A Cursor transcript exists on disk at ~/.cursor/projects/.../agent-transcripts/<uuid>/

Note: cursorLocalLauncher already calls onSessionFound(resumeChatId) when session.sessionId is set at launch; cursorRemoteLauncher does not — asymmetry.

Reproduction (2026-05-31)

  1. Spawn a cursor session with resume: cursor --resume d9c3d739-f146-434a-8339-16cfcb791422
  2. Terminate the agent within ~2s (before [CursorSession] Cursor session ID ... added to metadata appears in logs)
  3. Session archives with lifecycleState: archived, cursorSessionId absent in hub metadata
  4. POST /api/sessions/<id>/resume500 resume_unavailable
  5. Manual recovery: patch DB with cursorSessionId + hub restart → resume succeeds

Affected session (operator repro)

Field Value
HAPI session 0525fe34-2eab-4d8e-a4ed-ef8210d172b6 ("android watch")
Cursor resume id (spawn arg) d9c3d739-f146-434a-8339-16cfcb791422
Recovered after DB patch 2010b5cf-cb9e-404c-bf8e-c4968bb28e7b

Expected HTTP response

GET /api/cli/sessions/:id/resume-target already maps resume_unavailable409. POST /api/sessions/:id/resume returns 500 for the same code — inconsistent and misleading (looks like server fault vs. missing resume token).

Proposed fix

  1. CLI: Persist cursorSessionId as early as possible — when spawn already has --resume id (mirror local launcher), and on system init event (existing path).
  2. Hub: Map resume_unavailable409 on POST /sessions/:id/resume with actionable message (start fresh vs resume).
  3. Optional: Best-effort recovery from transcript path when metadata missing (cursor only).

Related

  • #728 — resume/config RPC race (resume_failed, not resume_unavailable)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions