jackwener/OpenCLI · error · CommandExecutionError
twitter_collection_invalid_timestamp
twitter_collection_invalid_timestamp
Error message
twitter_collection_invalid_timestamp: post ${post.id} What it means
parseCreatedAt validates each post's created_at field: it must be a non-empty string parseable to a valid Date. Posts without a usable timestamp cannot be compared against the 'until' cutoff, so completion of the collection can't be proven and the library throws this error naming the offending post id.
Solutions
- Skip or whitelist promoted/ad entries and tombstones in the visit() walker before calling parseCreatedAt.
- Log the offending post.id and its raw object to confirm which entry type lacks created_at.
- Add a fallback timestamp source (e.g. tweet.core.legacy.created_at or sort index) if X moved the field.
- Update the library if X changed the legacy payload shape.
- Retry pagination — transient timeline injections usually disappear on the next page fetch.
Example fix
// before
const createdAt = parseCreatedAt(post);
// after
if (!post?.created_at || Number.isNaN(new Date(post.created_at).getTime())) {
console.warn('skipping post without timestamp', post?.id);
continue;
}
const createdAt = parseCreatedAt(post); Defensive patterns
Strategy: type-guard
Validate before calling
const bad = posts.filter(p => typeof p?.created_at !== 'string' || !p.created_at || Number.isNaN(new Date(p.created_at).getTime()));
if (bad.length) console.warn('posts lacking valid created_at:', bad.map(p => p.id)); Type guard
function hasValidCreatedAt(post) {
return typeof post?.created_at === 'string' && post.created_at.length > 0
&& !Number.isNaN(new Date(post.created_at).getTime());
} Try / catch
try {
const createdAt = parseCreatedAt(post);
} catch (err) {
if (String(err.message).startsWith('twitter_collection_invalid_timestamp')) {
const id = String(err.message).split('post ')[1];
console.warn('skipping malformed post', id);
} else throw err;
} Prevention
- Filter promoted/ad/tombstone entries before timestamp parsing.
- Validate created_at is a parseable string for every entry you walk.
- Support alternate timestamp locations (tweet.core.legacy.created_at) as fallback.
- Keep the library updated when X renames legacy fields.
- Log offending post ids to identify which entry types lack timestamps.
When it happens
Trigger: A UserTweets entry whose tweet.legacy.created_at is missing, empty, or malformed (e.g. ad/promoted entries, tombstones reshaped into post objects, or X renaming legacy fields).
Common situations: Promoted/ad tweets injected into the timeline lacking legacy.created_at; X schema change renaming created_at; posts from retweets/quote cards with truncated legacy objects; account locale changes altering date format unexpectedly.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- twitter collection --until must be an RFC3339 timestamp
- 定时发布时间不能超过 14 天
- Bilibili view API did not return pages[] for --page…
- Could not parse Twitter likes resume file
- CreateList returned a list payload without a numeric list…
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/a176d25f605ef289.
Report an issue: GitHub.
Appendix: source
Thrown at clis/twitter/collection.js:240
) {
nextCursor = value.value;
}
if (Array.isArray(value)) {
for (const item of value) visit(item);
return;
}
for (const child of Object.values(value)) {
if (child && typeof child === 'object') visit(child);
}
};
for (const instructions of instructionSets) visit(instructions);
return [posts, nextCursor];
}
function parseCreatedAt(post) {
const parsed = new Date(post.created_at);
if (typeof post.created_at !== 'string' || !post.created_at || Number.isNaN(parsed.getTime())) {
throw new CommandExecutionError(`twitter_collection_invalid_timestamp: post ${post.id}`);
}
return parsed;
}
function completedReceipt(stopReason, until, pagesFetched, oldestSeenAt) {
return {
completed: true,
stop_reason: stopReason,
requested_until: until.toISOString(),
pages_fetched: pagesFetched,
oldest_seen_at: oldestSeenAt ? oldestSeenAt.toISOString() : null,
};
}
async function paginateCollection({ until, limit, maxPages = MAX_USER_TWEETS_PAGES, fetchPage, wait }) {
const seen = new Set();
const seenCursors = new Set();
const posts = [];View on GitHub (pinned to 49907e53dc)