{"record":{"id":"2ca7fc65910e0016","repo":"jackwener/OpenCLI","slug":"gemini-conversation-list-returned-a-malformed-row","errorCode":null,"errorMessage":"Gemini conversation list returned a malformed row","messagePattern":"Gemini conversation list returned a malformed row","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/gemini/utils.js","lineNumber":1151,"sourceCode":"export async function startNewGeminiChat(page) {\n    await ensureGeminiPage(page);\n    const action = await page.evaluate(clickNewChatScript());\n    if (action === 'navigate') {\n        await page.goto(GEMINI_APP_URL, { waitUntil: 'load', settleMs: 2500 });\n    }\n    await page.wait(1);\n    return action;\n}\nexport async function getGeminiConversationList(page) {\n    await ensureGeminiPage(page);\n    let rows = [];\n    // Gemini collapses the sidebar \"最近\"/Recents section by default, hiding\n    // the conversation anchors. Retry extraction after expanding it.\n    for (let attempt = 0; attempt < 3; attempt += 1) {\n        const raw = requireGeminiArrayResult(await page.evaluate(getGeminiConversationListScript()), 'Gemini conversation list');\n        rows = raw.flatMap((item) => {\n            if (!isObjectRecord(item) || typeof item.title !== 'string' || typeof item.url !== 'string') {\n                throw new CommandExecutionError('Gemini conversation list returned a malformed row');\n            }\n            if (!isGeminiConversationUrl(item.url))\n                return [];\n            return { Title: item.title, Url: item.url };\n        });\n        if (rows.length > 0)\n            break;\n        const changedRaw = unwrapGeminiEvaluateResult(await page.evaluate(expandGeminiRecentScript()), 'Gemini sidebar expand');\n        const changed = changedRaw === true;\n        // Give the React sidebar a moment to render the expanded anchors,\n        // longer on the first expansion (sidebar slide-in is async).\n        await page.wait(attempt === 0 ? 1.2 : 0.6);\n        if (!changed && attempt > 0)\n            break;\n    }\n    return rows;\n}\nexport async function clickGeminiConversationByTitle(page, query) {","sourceCodeStart":1133,"sourceCodeEnd":1169,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/gemini/utils.js#L1133-L1169","documentation":"Thrown by CommandExecutionError when the Gemini conversation-list extraction script returns a row that is not an object with string title and url fields. The library validates every row returned by getGeminiConversationListScript inside page.evaluate before mapping it to { Title, Url }, because the sidebar DOM can yield anchors that don't carry the expected data.","triggerScenarios":"Calling the conversation-list command when page.evaluate(getGeminiConversationListScript()) returns array entries missing title or url, or where those fields are non-strings (e.g. null entries from empty/placeholder sidebar items, sponsored rows, or a changed DOM shape).","commonSituations":"Gemini updated the Recents sidebar markup so the extraction script returns raw nodes instead of { title, url } objects; locale-specific sidebar entries with empty titles; mocked/partial test data; scraping while the sidebar is still loading.","solutions":["Wait for the sidebar Recents section to finish loading (the code already retries 3x after expanding it — check whether retries were exhausted).","Inspect one raw row returned by getGeminiConversationListScript and update the extraction script to the current sidebar DOM shape.","Filter out placeholder/empty rows inside the in-page script before returning them.","Wrap the call in try/catch for CommandExecutionError and degrade gracefully to an empty list."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isConversationRow(row) {\n  return typeof row === 'object' && row !== null &&\n    typeof row.title === 'string' && typeof row.url === 'string';\n}","tryCatchPattern":"let conversations = [];\ntry {\n  conversations = await listGeminiConversations(page);\n} catch (e) {\n  if (String(e.message).includes('malformed row')) conversations = []; // degrade gracefully\n  else throw e;\n}","preventionTips":["Only read the conversation list after the sidebar Recents section has rendered.","Allow the library's 3-attempt expansion retry to complete before giving up.","Update extraction selectors whenever Gemini changes the sidebar markup.","Treat the list as best-effort data; never hard-depend on it."],"tags":["gemini","dom-scraping","schema-validation","page-evaluate"],"backgroundTag":"schema-validation-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}