{"record":{"id":"93fb57ce8cb195a9","repo":"jackwener/OpenCLI","slug":"twitter-bookmarks-resume-file-filepath-has-an-i","errorCode":null,"errorMessage":"Twitter bookmarks resume file ${filePath} has an invalid shape","messagePattern":"Twitter bookmarks resume file (.+?) has an invalid shape","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/twitter/bookmarks.js","lineNumber":122,"sourceCode":"}\nfunction readResumeFile(filePath, expected = null) {\n    if (!filePath || !fs.existsSync(filePath))\n        return null;\n    let parsed;\n    try {\n        parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));\n    }\n    catch (error) {\n        throw new CommandExecutionError(`Could not parse Twitter bookmarks resume file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);\n    }\n    const count = parsed?.count;\n    const cursor = parsed?.cursor == null ? null : String(parsed.cursor);\n    const outputFile = parsed?.outputFile ? path.resolve(String(parsed.outputFile)) : null;\n    if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)\n        || !Number.isInteger(count) || count < 0\n        || (parsed.cursor != null && typeof parsed.cursor !== 'string')\n        || (cursor !== null && !cursor.trim())) {\n        throw new CommandExecutionError(`Twitter bookmarks resume file ${filePath} has an invalid shape`);\n    }\n    if (expected) {\n        if (parsed.source !== expected.source)\n            throw new ArgumentError(`Resume file source mismatch: expected ${expected.source}, found ${parsed.source || 'unknown'}`);\n        if (outputFile !== expected.outputFile)\n            throw new ArgumentError(`Resume file output mismatch: expected ${expected.outputFile || 'in-memory mode'}, found ${outputFile || 'in-memory mode'}`);\n        if (!expected.outputFile && !Array.isArray(parsed.tweets))\n            throw new CommandExecutionError(`Twitter bookmarks resume file ${filePath} is missing in-memory tweets`);\n        if (!expected.outputFile && parsed.tweets.length !== count)\n            throw new CommandExecutionError(`Twitter bookmarks resume file ${filePath} count does not match its in-memory tweets`);\n        if (parsed.complete)\n            throw new CommandExecutionError(`Twitter bookmarks resume file ${filePath} is already marked complete`);\n    }\n    return {\n        cursor,\n        count,\n        tweets: Array.isArray(parsed.tweets) ? parsed.tweets : [],\n        complete: Boolean(parsed.complete),","sourceCodeStart":104,"sourceCodeEnd":140,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/bookmarks.js#L104-L140","documentation":"After parsing, readResumeFile validates the resume file's shape: it must be a non-null, non-array object with an integer count >= 0, a cursor that is null or a non-empty string, and no blank-string cursor. A file that parses as JSON but fails this contract throws this CommandExecutionError.","triggerScenarios":"Passing a --resume-file that contains valid JSON of the wrong structure — e.g. `{}`, an array `[]`, `\"text\"`, `\"{\"count\":\"5\"}` (count not an integer), a negative count, a numeric cursor, or a cursor of `\"\"` or whitespace.","commonSituations":"Pointing --resume-file at the JSONL output-file instead of the resume JSON; hand-editing the resume file and changing field types; an older/other command wrote a different resume schema to the same path.","solutions":["Inspect the file and ensure it is an object with integer `count` (>=0) and optional string non-blank `cursor`.","Delete the malformed file and restart the --all sync so the tool writes a fresh resume file.","Verify you passed the resume file path, not the JSONL output-file path, to --resume-file."],"exampleFix":"// before: resume.json = {\"count\": \"5\", \"cursor\": 123}\n// after (valid shape)\n{\"count\": 5, \"cursor\": \"ABCDEF\", \"source\": \"bookmarks\", \"tweets\": [], \"complete\": false}","handlingStrategy":"validation","validationCode":"function resumeShapeOk(p) {\n  const o = JSON.parse(fs.readFileSync(p, 'utf8'));\n  return o && typeof o === 'object' && !Array.isArray(o)\n    && Number.isInteger(o.count) && o.count >= 0\n    && (o.cursor == null || (typeof o.cursor === 'string' && o.cursor.trim() !== ''));\n}","typeGuard":"function looksLikeResumeState(v) {\n  return typeof v === 'object' && v !== null && !Array.isArray(v)\n    && Number.isInteger(v.count) && v.count >= 0;\n}","tryCatchPattern":"try {\n  await runBookmarks({ resumeFile });\n} catch (e) {\n  if (String(e.message).endsWith('has an invalid shape')) {\n    fs.rmSync(resumeFile, { force: true });\n    await runBookmarks({ resumeFile }); // fresh start\n  } else throw e;\n}","preventionTips":["Never point --resume-file at the JSONL --output-file; use separate paths.","Do not hand-edit field types in the resume JSON.","Use one schema version of the tool per resume file.","Prefer --output-file mode for long syncs so the archive, not in-memory tweets, carries the data."],"tags":["schema-validation","resume-file","cli"],"backgroundTag":"resume-file-invalid-shape","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}