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)
- Spawn a cursor session with resume:
cursor --resume d9c3d739-f146-434a-8339-16cfcb791422
- Terminate the agent within ~2s (before
[CursorSession] Cursor session ID ... added to metadata appears in logs)
- Session archives with
lifecycleState: archived, cursorSessionId absent in hub metadata
POST /api/sessions/<id>/resume → 500 resume_unavailable
- 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_unavailable → 409. POST /api/sessions/:id/resume returns 500 for the same code — inconsistent and misleading (looks like server fault vs. missing resume token).
Proposed fix
- CLI: Persist
cursorSessionId as early as possible — when spawn already has --resume id (mirror local launcher), and on system init event (existing path).
- Hub: Map
resume_unavailable → 409 on POST /sessions/:id/resume with actionable message (start fresh vs resume).
- Optional: Best-effort recovery from transcript path when metadata missing (cursor only).
Related
- #728 — resume/config RPC race (
resume_failed, not resume_unavailable)
Summary
POST /api/sessions/:id/resumereturns HTTP 500 with{"code":"resume_unavailable","error":"Resume session ID unavailable"}when resuming an inactive/archived cursor session whose hub metadata never receivedcursorSessionId.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 initstream event.Root cause
syncEngine.resolveLocalResumeTarget()requiresmetadata.cursorSessionIdfor cursor flavor (hub/src/sync/syncEngine.ts):CursorSession.onSessionFound()persists the ID viaupdateMetadata, but remote cursor only calls it when the agent stream emitssystem/initwithsession_id. If the process exits or is terminated before that event, metadata stays empty even though:--resume <uuid>(ID known at spawn)~/.cursor/projects/.../agent-transcripts/<uuid>/Note:
cursorLocalLauncheralready callsonSessionFound(resumeChatId)whensession.sessionIdis set at launch;cursorRemoteLauncherdoes not — asymmetry.Reproduction (2026-05-31)
cursor --resume d9c3d739-f146-434a-8339-16cfcb791422[CursorSession] Cursor session ID ... added to metadataappears in logs)lifecycleState: archived,cursorSessionIdabsent in hub metadataPOST /api/sessions/<id>/resume→ 500resume_unavailablecursorSessionId+ hub restart → resume succeedsAffected session (operator repro)
0525fe34-2eab-4d8e-a4ed-ef8210d172b6("android watch")d9c3d739-f146-434a-8339-16cfcb7914222010b5cf-cb9e-404c-bf8e-c4968bb28e7bExpected HTTP response
GET /api/cli/sessions/:id/resume-targetalready mapsresume_unavailable→ 409.POST /api/sessions/:id/resumereturns 500 for the same code — inconsistent and misleading (looks like server fault vs. missing resume token).Proposed fix
cursorSessionIdas early as possible — when spawn already has--resumeid (mirror local launcher), and onsystem initevent (existing path).resume_unavailable→ 409 onPOST /sessions/:id/resumewith actionable message (start fresh vs resume).Related
resume_failed, notresume_unavailable)