jackwener/OpenCLI · error · CommandExecutionError

Twitter device-follow entries could not be joined to tweet/u

Error message

Twitter device-follow entries could not be joined to tweet/user objects.

What it means

parseDeviceFollow returns counts of entries that were malformed or could not be joined back to their tweet/user objects in globalObjects. If any exist, the command refuses to return partial/garbage data and throws CommandExecutionError instead.

Source

Thrown at clis/twitter/device-follow.js:176

      }`);
        if (data?.errorKind === 'non_json') {
            throw new CommandExecutionError(`Twitter device-follow returned non-JSON response: ${data.detail || 'unknown parse error'}`);
        }
        if (data?.errorKind === 'exception') {
            throw new CommandExecutionError(`Twitter device-follow fetch failed: ${data.detail || 'unknown error'}`);
        }
        if (data?.error) {
            if (data.error === 401 || data.error === 403) {
                throw new AuthRequiredError('x.com', `Twitter device-follow returned HTTP ${data.error}`);
            }
            throw new CommandExecutionError(describeTwitterApiError('device_follow', data.error));
        }
        const parsed = parseDeviceFollow(data, new Set());
        if (!parsed) {
            throw new CommandExecutionError('Twitter device-follow response was missing the expected timeline/globalObjects shape.');
        }
        if (parsed.malformedEntries > 0 || parsed.unmatchedTweetEntries > 0) {
            throw new CommandExecutionError('Twitter device-follow entries could not be joined to tweet/user objects.');
        }
        if (parsed.rows.length === 0) {
            throw new EmptyResultError('twitter device-follow', 'No device-follow notification tweets found.');
        }
        const rows = parsed.rows;
        const trimmed = rows.slice(0, limit);
        return applyTopByEngagement(trimmed, kwargs['top-by-engagement']);
    },
});

export const __test__ = {
    buildDeviceFollowUrl,
    extractEntries,
    joinEntryToTweet,
    shapeRow,
    parseDeviceFollow,
    parseLimit,
};

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Retry — deleted/tombstoned entries are sometimes transient
  2. Update the CLI so the parser/joiner handles the newest entry shapes
  3. Narrow the query (smaller --limit, different account) to avoid stale notification entries
  4. Inspect the raw payload to confirm which entries fail to join
Defensive patterns

Strategy: try-catch

Validate before calling

// No caller-side pre-check; reduce risk by using smaller limits on accounts with stale notifications
const limit = 50;

Try / catch

try {
  await cli.twitter.deviceFollow();
} catch (e) {
  if (e instanceof CommandExecutionError && /joined to tweet\/user objects/.test(e.message)) {
    console.error('Some notifications reference deleted tweets/users — retry or lower --limit.');
    process.exitCode = 1;
  } else throw e;
}

Prevention

When it happens

Trigger: The response contains timeline entries whose tweet or user IDs are missing from globalObjects.tweets/globalObjects.users (deleted tweets, pruned users, tombstoned entries), or entries with unexpected internal structure.

Common situations: Notifications referencing tweets that were deleted or from suspended accounts; Twitter tombstoning entries in the timeline; partially truncated payloads; new entry types the joiner doesn't recognize.

Related errors


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