{"record":{"id":"9c8fde00151d17a4","repo":"jackwener/OpenCLI","slug":"sales-navigator-messaging-thread-api-returned-part","errorCode":null,"errorMessage":"Sales Navigator messaging thread API returned partial history (${have}/${total})","messagePattern":"Sales Navigator messaging thread API returned partial history \\((.+?)/(.+?)\\)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/linkedin/salesnav-thread.js","lineNumber":165,"sourceCode":"\nasync function fetchThreadWithPagination(page, csrf, threadId, limit = DEFAULT_MESSAGE_LIMIT) {\n  let requested = THREAD_PAGE_SIZE;\n  if (limit < requested) requested = limit;\n  let thread = null;\n  for (let attempts = 0; attempts < 30; attempts += 1) {\n    thread = await fetchSalesnavJson(page, csrf, threadApiUrl(threadId, requested), 'Sales Navigator messaging thread API');\n    const total = Number(thread?.totalMessageCount || 0);\n    const have = Array.isArray(thread?.messages) ? thread.messages.length : 0;\n    if (have >= limit || (total && have >= total) || requested >= limit) break;\n    let nextRequested = requested + THREAD_PAGE_SIZE;\n    if (total && total > nextRequested) nextRequested = total;\n    if (nextRequested > limit) nextRequested = limit;\n    requested = nextRequested;\n  }\n  const total = Number(thread?.totalMessageCount || 0);\n  const have = Array.isArray(thread?.messages) ? thread.messages.length : 0;\n  if (total && have < total && have < limit) {\n    throw new CommandExecutionError(`Sales Navigator messaging thread API returned partial history (${have}/${total})`);\n  }\n  return thread;\n}\n\ncli({\n  site: 'linkedin',\n  name: 'salesnav-thread',\n  access: 'read',\n  description: 'Return full Sales Navigator message history for a thread id, Sales Navigator inbox URL, lead URL, recipient urn, or exact recipient name',\n  domain: LINKEDIN_DOMAIN,\n  strategy: Strategy.UI,\n  browser: true,\n  args: [\n    { name: 'thread-or-recipient', type: 'string', required: true, positional: true, help: 'Sales Navigator inbox URL/thread id, Sales Navigator lead URL, recipient urn, or exact participant name' },\n    { name: 'limit', type: 'number', default: DEFAULT_MESSAGE_LIMIT, help: 'Maximum messages to return (1-500)' },\n    { name: 'max-pages', type: 'number', default: 30, help: 'Maximum inbox pages to scan when resolving a recipient' },\n  ],\n  columns: ['index', 'thread_id', 'thread_url', 'sender', 'text', 'timestamp', 'subject', 'message_id', 'sender_urn', 'delivered_at', 'type', 'total_message_count'],","sourceCodeStart":147,"sourceCodeEnd":183,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/linkedin/salesnav-thread.js#L147-L183","documentation":"fetchThreadWithPagination keeps requesting older message pages until the requested count reaches the limit. After fetching it compares LinkedIn's reported totalMessageCount with the number of messages actually collected; if fewer were returned than both the total and the requested limit, it throws CommandExecutionError about partial history. The library refuses to silently return an incomplete conversation.","triggerScenarios":"LinkedIn's messaging thread API stops returning further pages before totalMessageCount is reached (e.g. an empty or truncated next-page cursor, deleted/hidden messages, or an API response that reports a larger total than it will actually serve), while have < the requested limit.","commonSituations":"Threads where LinkedIn has hidden or deleted some older messages so the real retrievable count is below totalMessageCount; transient API flakiness returning a final page early; very long threads where pagination ends prematurely after a LinkedIn messaging API change.","solutions":["Retry the command; transient truncation often resolves on a second run.","Raise --limit so have is no longer below the limit used in the check (the error only fires when have < limit).","Inspect the thread in Sales Navigator to confirm messages were deleted/hidden; if so, accept the partial history by handling the error and using what was fetched on a later attempt with a smaller limit.","Report/pin the LinkedIn API version if the thread API recently changed behavior."],"exampleFix":"// before\nthread = await fetchThreadWithPagination(page, csrf, threadId, 500) // partial -> throws\n// after\ntry {\n  thread = await fetchThreadWithPagination(page, csrf, threadId, 500)\n} catch (e) {\n  if (/partial history/.test(e.message)) {\n    thread = await fetchThreadWithPagination(page, csrf, threadId, 50) // smaller limit avoids have<limit trigger\n  } else throw e\n}","handlingStrategy":"retry","validationCode":"null","typeGuard":"null","tryCatchPattern":"try { thread = await thread() } catch (e) { if (/partial history \\(\\d+\\/\\d+\\)/.test(e.message)) { await sleep(1000); thread = await thread(); /* or accept partial with lower limit */ } else throw e; }","preventionTips":["Retry transient truncations before treating them as permanent.","Compare fetched count vs totalMessageCount yourself and log mismatches.","Keep --limit realistic; the error only fires when have < limit.","Watch LinkedIn messaging API changelog for pagination regressions."],"tags":["pagination","linkedin","sales-navigator","api-inconsistency"],"backgroundTag":"incomplete-paginated-response","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}