{"record":{"id":"937cd2fe4cdef836","repo":"jackwener/OpenCLI","slug":"twitter-post","errorCode":null,"errorMessage":"twitter post","messagePattern":"twitter post","errorType":"exception","errorClass":"TimeoutError","httpStatus":null,"severity":"warning","filePath":"clis/twitter/post.js","lineNumber":352,"sourceCode":"            if (!uploadState?.ok) {\n                throw new TimeoutError('twitter image upload', UPLOAD_TIMEOUT_MS / 1000, 'Nothing was posted. Retry, or attach a smaller image.');\n            }\n        }\n\n        // Insert and verify the text after media upload so text + images are in\n        // the final Draft.js composer state immediately before clicking Post.\n        const typeResult = await insertComposerText(page, text);\n        if (!typeResult?.ok) {\n            throw new CommandExecutionError(typeResult?.message ?? 'Could not type tweet text.', 'Open the composer in the browser and check whether X is asking you to log in.');\n        }\n\n        await page.wait(1);\n        const result = await submitTweet(page, text);\n        if (result?.unconfirmed) {\n            // The poll expiring does not mean the tweet stayed in the composer,\n            // so this must not read as a definite failure: the agent workflow\n            // retries CommandExecutionError and would post twice (#2255).\n            throw new TimeoutError('twitter post', SUBMIT_TIMEOUT_MS / 1000, `${result.message} Check \\`opencli twitter tweets --limit 1\\` before retrying; the post may already be live.`);\n        }\n        if (!result?.ok) {\n            throw new CommandExecutionError(result?.message ?? 'Tweet failed to post.', 'Nothing was posted. Open the composer in the browser and retry.');\n        }\n        return [{\n            status: 'success',\n            message: result.message,\n            text,\n            ...(result.id ? { id: result.id } : {}),\n            ...(result.url ? { url: result.url } : {}),\n        }];\n    }\n});\n","sourceCodeStart":334,"sourceCodeEnd":366,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/post.js#L334-L366","documentation":"TimeoutError thrown when submitTweet could not CONFIRM the tweet went out before SUBMIT_TIMEOUT_MS. Crucially this is ambiguous: the poll expiring does not mean the tweet failed — it may already be live. Retrying blindly risks a duplicate post (issue #2255), so the message directs you to check `opencli twitter tweets --limit 1` first.","triggerScenarios":"submitTweet returns { unconfirmed: true } — clicking Post did not yield a confirmation (toast/permalink) within SUBMIT_TIMEOUT_MS, e.g. slow X response, network hiccup after the click, or confirmation UI changes.","commonSituations":"Slow network right after clicking Post; X confirmation toast delayed or suppressed; agent automation retry loops causing duplicate tweets when the error is treated as a definite failure.","solutions":["Do NOT immediately retry; run `opencli twitter tweets --limit 1` to see if the post is already live","If the tweet is live, treat the command as successful and skip retry","If not live, retry the post command once","Increase the submit timeout or improve network conditions if this recurs","If confirmations are consistently missed, X may have changed its success indicators — report/update selectors"],"exampleFix":"// before\ntry { await post(); } catch (e) { await post(); } // may double-post\n// after\ntry { await post(); } catch (e) {\n  const recent = await opencli('twitter tweets', { limit: 1 });\n  if (!recent.some(t => t.text === expectedText)) await post();\n}","handlingStrategy":"retry","validationCode":"// before retrying, verify the post actually went out\nconst recent = await opencli('twitter tweets', { limit: 1 });\nif (recent?.[0]?.text === expectedText) return; // already live, skip retry","typeGuard":"const isAmbiguousSubmitTimeout = (e) =>\n  e?.name === 'TimeoutError' && e.message.startsWith('twitter post');","tryCatchPattern":"try {\n  await postTweet({ text });\n} catch (e) {\n  if (isAmbiguousSubmitTimeout(e)) {\n    const recent = await opencli('twitter tweets', { limit: 1 });\n    if (recent?.[0]?.text !== text) await postTweet({ text });\n    return;\n  }\n  throw e;\n}","preventionTips":["Never blind-retry on this timeout — check tweets --limit 1 first (duplicate risk, #2255)","Deduplicate by comparing the most recent tweet's text before re-posting","Post on a stable network connection","Track posted texts in a local ledger to detect duplicates"],"tags":["timeout","twitter","duplicate-prevention"],"backgroundTag":"ambiguous-submit-timeout","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}