jackwener/OpenCLI · error · CommandExecutionError

Twitter bookmarks output file is missing for resume state: $

Error message

Twitter bookmarks output file is missing for resume state: ${outputFile}

What it means

During a resumed archive run, the resume file records a nonzero count of records already written, but the JSONL output file it points to no longer exists on disk. clis/twitter/bookmarks.js:220 throws CommandExecutionError because continuing would produce an archive with a gap; the on-disk file must exist to match the resume state.

Source

Thrown at clis/twitter/bookmarks.js:220

            throw new ArgumentError('--output-file requires --resume-file so partial archives remain resumable');
        }
        const cookies = await page.getCookies({ url: 'https://x.com' });
        const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;
        if (!ct0)
            throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
        const queryId = await resolveTwitterQueryId(page, 'Bookmarks', BOOKMARKS_QUERY_ID);
        const headers = JSON.stringify({
            'Authorization': `Bearer ${decodeURIComponent(TWITTER_BEARER_TOKEN)}`,
            'X-Csrf-Token': ct0,
            'X-Twitter-Auth-Type': 'OAuth2Session',
            'X-Twitter-Active-User': 'yes',
        });
        const resumed = fetchAll ? readResumeFile(resumeFile, {
            source: 'bookmarks',
            outputFile: useOutputFile ? outputFile : null,
        }) : null;
        if (useOutputFile && resumed && resumed.count > 0 && !fs.existsSync(outputFile)) {
            throw new CommandExecutionError(`Twitter bookmarks output file is missing for resume state: ${outputFile}`);
        }
        if (useOutputFile && !resumed && fs.existsSync(outputFile)) {
            throw new ArgumentError(`Refusing to overwrite existing Twitter bookmarks output file: ${outputFile}`);
        }
        const allTweets = useOutputFile ? [] : (resumed?.tweets ? [...resumed.tweets] : []);
        const jsonlState = useOutputFile ? loadJsonlArchiveState(outputFile) : null;
        const seen = useOutputFile
            ? jsonlState.seen
            : new Set(allTweets.map((tweet) => tweet?.id).filter(Boolean));
        if (useOutputFile && resumed && jsonlState.count !== resumed.count) {
            throw new CommandExecutionError(`Twitter bookmarks output file has ${jsonlState.count} record(s), expected resume count ${resumed.count}`);
        }
        let outputCount = useOutputFile ? jsonlState.count : 0;
        let cursor = resumed?.cursor || null;
        let pages = 0;
        let exhausted = false;
        // Runaway guard only; --limit/--all and cursor exhaustion control normal pagination.
        while (pages < maxPages && (fetchAll || allTweets.length < limit)) {

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Restore the output file to the recorded path (restore from backup/trash) and re-run with --all --output-file --resume-file.
  2. Delete the stale resume file and start the archive from scratch (accepting re-fetch).
  3. Pass the exact same --output-file path used in the original run so path resolution matches the resume record.
  4. Check for typos or a changed working directory in the --output-file argument.

Example fix

// before
rm bookmarks.jsonl   # resume file left behind
twitter bookmarks --all --output-file bookmarks.jsonl --resume-file bookmarks.resume.json
// after: start clean when archive is unrecoverable
rm bookmarks.jsonl bookmarks.resume.json
twitter bookmarks --all --output-file bookmarks.jsonl --resume-file bookmarks.resume.json
Defensive patterns

Strategy: validation

Validate before calling

const resume = JSON.parse(fs.readFileSync(resumeFile, 'utf8'));
if (resume.count > 0 && resume.outputFile && !fs.existsSync(resume.outputFile)) {
  throw new Error(`Output file missing for resume state: ${resume.outputFile}`);
}

Type guard

function isResumableArchive(resume, outputFile) {
  return !(resume && resume.count > 0) || (typeof outputFile === 'string' && fs.existsSync(outputFile));
}

Try / catch

try {
  await runBookmarks(argv);
} catch (e) {
  if (e instanceof CommandExecutionError && e.message.includes('output file is missing for resume state')) {
    console.error('Restore the .jsonl archive or delete the stale .resume.json to start fresh.');
    process.exitCode = 1;
  } else throw e;
}

Prevention

When it happens

Trigger: Resume file has resumed.count > 0 and an outputFile recorded, but `fs.existsSync(outputFile)` is false when the run resumes — detected immediately before fetching.

Common situations: User deleted or moved the partial archive file but kept the resume file; output path changed between runs (different cwd, renamed file); cleanup job removed the .jsonl but not the .resume.json; output mismatch check was passed because paths resolve differently.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/67535bac922d70a8. Report an issue: GitHub.