{"record":{"id":"af18744531a179f7","repo":"jackwener/OpenCLI","slug":"sales-navigator-messaging-thread-api-returned-a-th","errorCode":null,"errorMessage":"Sales Navigator messaging thread API returned a thread without id","messagePattern":"Sales Navigator messaging thread API returned a thread without id","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/linkedin/salesnav-thread.js","lineNumber":91,"sourceCode":"\nfunction participantIndex(thread) {\n  const resolution = thread?.participantsResolutionResults || {};\n  const participants = Array.isArray(thread?.participants) ? thread.participants : Object.keys(resolution);\n  const byUrn = new Map();\n  for (const urn of participants) {\n    const profile = resolution[urn] || { entityUrn: urn };\n    byUrn.set(urn, profile);\n  }\n  return byUrn;\n}\n\nfunction parseSalesnavThreadMessages(thread) {\n  if (!thread || typeof thread !== 'object') {\n    throw new CommandExecutionError('Sales Navigator messaging thread API returned malformed payload');\n  }\n  const threadId = normalizeWhitespace(thread?.id || '');\n  if (!threadId) {\n    throw new CommandExecutionError('Sales Navigator messaging thread API returned a thread without id');\n  }\n  if (!Array.isArray(thread?.messages)) {\n    throw new CommandExecutionError('Sales Navigator messaging thread API returned malformed messages');\n  }\n  const byUrn = participantIndex(thread);\n  const messages = thread.messages;\n  const rows = messages.map((message) => {\n    if (!message || typeof message !== 'object') {\n      throw new CommandExecutionError('Sales Navigator messaging thread API returned malformed message row');\n    }\n    const deliveredAt = Number(message?.deliveredAt || 0);\n    const senderProfile = byUrn.get(message?.author);\n    return {\n      message_id: normalizeWhitespace(message?.id || ''),\n      thread_id: threadId,\n      sender: participantName(senderProfile) || normalizeWhitespace(message?.author || ''),\n      sender_urn: normalizeWhitespace(message?.author || ''),\n      text: normalizeWhitespace(message?.body || message?.systemMessageContent || ''),","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/linkedin/salesnav-thread.js#L73-L109","documentation":"Thrown by parseSalesnavThreadMessages (clis/linkedin/salesnav-thread.js:91) when the thread object exists but its id field is missing or whitespace-only after normalization. The id is required to build thread URLs, correlate messages, and anchor each output row's thread_id. The library treats an id-less thread as a malformed response rather than silently returning rows without an identifier.","triggerScenarios":"LinkedIn returned a thread-shaped object whose id was absent (decoration mismatch dropping the id field); fetchSalesnavJson parsed a partial/error payload that still looked like an object; passing a thread object fetched from a different endpoint that uses a different id key (e.g. entityUrn instead of id).","commonSituations":"A Sales Navigator redeploy renaming or relocating the id field under a new decoration version; reusing this parser on thread objects from a different API (inbox listing vs thread detail) whose id lives in another property; truncated responses due to session limits.","solutions":["Re-run the command to rule out a transient partial response.","Verify the THREAD_DECORATION version still matches the live /sales-api/salesApiThreads response shape; refresh from the network tab if LinkedIn changed it.","Ensure you are parsing the thread-detail response (which carries id), not an inbox-list row where the id may live in a different field like entityUrn.","Log the raw thread object (Object.keys(thread)) before parsing to identify the actual id field name and adapt or report the mismatch."],"exampleFix":"// before (parsing an inbox row that stores id elsewhere)\nparseSalesnavThreadMessages(inboxRow);\n// after (normalize the id before parsing)\nconst thread = { ...threadDetail, id: threadDetail.id || threadDetail.entityUrn?.split(':').pop() };\nparseSalesnavThreadMessages(thread);","handlingStrategy":"type-guard","validationCode":"// Ensure an id exists before parsing:\nconst threadId = typeof thread?.id === 'string' ? thread.id.trim() : '';\nif (!threadId) throw new Error('thread payload has no id; verify endpoint/decoration');","typeGuard":"function hasThreadId(t) {\n  return t !== null && typeof t === 'object' &&\n    typeof t.id === 'string' && t.id.trim().length > 0;\n}","tryCatchPattern":"try {\n  const messages = parseSalesnavThreadMessages(thread);\n} catch (err) {\n  if (/thread without id/.test(err.message) && thread?.entityUrn) {\n    // normalize alternate id field, then retry the parse\n    return parseSalesnavThreadMessages({ ...thread, id: thread.entityUrn.split(':').pop() });\n  }\n  throw err;\n}","preventionTips":["Parse only thread-detail responses with this function; inbox-list rows keep the id in a different field.","Inspect Object.keys(thread) when this fires — a renamed id field signals a decoration version bump.","Keep THREAD_DECORATION current with the live /sales-api/salesApiThreads response."],"tags":["linkedin","sales-navigator","api-response","schema-validation","missing-field"],"backgroundTag":"schema-validation-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}