{"record":{"id":"d47aaf9a1ec1d53b","repo":"nexu-io/open-design","slug":"empty-transcript","errorCode":"EMPTY_TRANSCRIPT","errorMessage":"conversation ${options.conversationId} has no messages to hand off","messagePattern":"conversation (.+?) has no messages to hand off","errorType":"http","errorClass":"EmptyTranscriptError","httpStatus":400,"severity":"error","filePath":"apps/daemon/src/design/handoff-design.ts","lineNumber":180,"sourceCode":"): Promise<HandoffResponse> {\n  const project = getProject(db, projectId);\n  if (!project) {\n    throw new Error(`project not found: ${projectId}`);\n  }\n\n  const now = options.now ?? (() => new Date());\n  const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;\n  const maxTokens = options.maxTokens ?? DEFAULT_MAX_TOKENS;\n\n  const transcriptResult = exportProjectTranscript(db, projectsRoot, projectId, {\n    now,\n    conversationId: options.conversationId,\n  });\n  // Fail fast on an empty conversation: synthesizing a handoff from zero\n  // messages would spend BYOK tokens to fabricate context that does not\n  // exist. The route maps EmptyTranscriptError to 400.\n  if (transcriptResult.messageCount === 0) {\n    throw new EmptyTranscriptError(\n      `conversation ${options.conversationId} has no messages to hand off`,\n    );\n  }\n  const transcriptJsonl = fs.readFileSync(transcriptResult.path, 'utf8');\n  const truncatedJsonl = truncateTranscriptForPrompt(transcriptJsonl);\n\n  const { systemPrompt, userPrompt } = buildHandoffPrompt({\n    projectId,\n    transcriptJsonl: truncatedJsonl,\n    transcriptMessageCount: transcriptResult.messageCount,\n    now: now(),\n  });\n\n  const timeoutController = new AbortController();\n  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n  // The timeout must stay armed until the response BODY has been fully\n  // read, not just until headers arrive. `fetch()` resolves as soon as the\n  // upstream sends headers, so clearing the timeout before `response.json()`","sourceCodeStart":162,"sourceCodeEnd":198,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/design/handoff-design.ts#L162-L198","documentation":"EmptyTranscriptError: exportProjectTranscript returned messageCount === 0 for the given conversationId. synthesizeHandoffPrompt refuses to spend BYOK tokens fabricating a resume prompt from an empty conversation. The route maps this to 400 EMPTY_TRANSCRIPT.","triggerScenarios":"Calling synthesizeHandoffPrompt (POST /api/projects/:id/handoff) with a conversationId that has zero messages — a freshly created conversation, a conversation whose messages were all deleted, or a wrong/mismatched conversationId.","commonSituations":"User clicked 'handoff' on a brand-new conversation before sending any message; conversationId doesn't match any real conversation; messages were cleared; the conversation belongs to a different project.","solutions":["Send at least one message in the conversation before requesting a handoff.","Verify the conversationId is correct and belongs to the project.","Check the conversation has messages via the chat history API before calling handoff."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Confirm the conversation has messages before requesting a handoff.\nimport { exportProjectTranscript } from '../transcript-export.js';\nconst probe = exportProjectTranscript(db, projectsRoot, projectId,\n  { now: () => new Date(), conversationId });\nif (probe.messageCount === 0) {\n  return res.status(400).json({ code: 'EMPTY_TRANSCRIPT', error: 'send a message first' });\n}","typeGuard":"import { EmptyTranscriptError } from './handoff-design.js';\nfunction isEmptyTranscript(err: unknown): err is EmptyTranscriptError {\n  return err instanceof EmptyTranscriptError;\n}","tryCatchPattern":"import { EmptyTranscriptError } from './handoff-design.js';\ntry {\n  const handoff = await synthesizeHandoffPrompt(db, projectsRoot, projectId, options);\n} catch (err) {\n  if (err instanceof EmptyTranscriptError) {\n    // Route maps to 400 EMPTY_TRANSCRIPT; prompt the user to send a message first.\n    return res.status(400).json({ code: 'EMPTY_TRANSCRIPT', error: err.message });\n  }\n  throw err;\n}","preventionTips":["Disable the handoff UI action until the conversation has at least one message.","Verify the conversationId belongs to the project before calling.","Fetch the conversation's message count via the chat API as a pre-check."],"tags":["handoff","transcript","empty","validation","conversation"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}