jackwener/OpenCLI · error · CommandExecutionError

Gmail thread pagination page ${pageNumber} repeated an earli

Error message

Gmail thread pagination page ${pageNumber} repeated an earlier response; refusing partial results

What it means

During pagination, if a page after the first adds zero new thread IDs (all rows were already seen), the library assumes the UI stopped advancing pages and throws CommandExecutionError rather than returning silently truncated results. It protects callers from partial data disguised as a complete result.

Source

Thrown at clis/gmail/utils.js:537

        if (clicked !== true) break;
      }
    }
    const bodies = await waitGmailCaptures(
      page,
      'bv',
      pageNumber === 1 ? 'thread list' : `thread pagination page ${pageNumber}`,
    );
    const pageRows = bodies.flatMap(parseBatchView);
    let added = 0;
    for (const row of pageRows) {
      if (seen.has(row.threadId)) continue;
      seen.add(row.threadId);
      rows.push(row);
      added += 1;
      if (rows.length >= limit) return rows;
    }
    if (pageNumber > 1 && added === 0) {
      throw new CommandExecutionError(`Gmail thread pagination page ${pageNumber} repeated an earlier response; refusing partial results`);
    }
    if (pageRows.length < PAGE_SIZE) break;
  }
  if (rows.length === 0) {
    throw new EmptyResultError('gmail search', `No threads matched "${normalizedQuery}"`);
  }
  return rows.slice(0, limit);
}

export async function listLabels(page, account = 0) {
  await ensureGmailReady(page, account, 'labels');
  await installGmailCapture(page, account, 'bv', 'labels');
  await submitSearch(page, 'in:anywhere', 'labels');
  const bodies = await waitGmailCaptures(page, 'bv', 'labels');
  const labels = bodies.flatMap(parseLabels);
  const fallback = labels.length === 0 ? await renderedLabels(page, account) : [];
  const unique = [...new Map([...labels, ...fallback].map((row) => [row.id, row])).values()];
  if (unique.length === 0) throw new EmptyResultError('gmail labels', 'Gmail returned no labels');

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Lower the limit so it fits within one page (PAGE_SIZE) of results
  2. Retry the search once — the failure is often transient UI state
  3. If persistent, the query may genuinely have fewer unique results than requested; reduce expectations or refine the query
  4. Verify the library/browser setup matches a supported Gmail UI version

Example fix

// before
const rows = await queryThreads(page, 'label:unread', { limit: 200 });
// after
const rows = await queryThreads(page, 'label:unread', { limit: 50 }); // fit within one page or retry on CommandExecutionError
Defensive patterns

Strategy: retry

Validate before calling

const SAFE_LIMIT = 50; // one page
const limit = Math.min(requestedLimit, SAFE_LIMIT);

Type guard

const fitsOnePage = (n) => Number.isInteger(n) && n > 0 && n <= 50;

Try / catch

try { rows = await queryThreads(page, q, { limit }); }
catch (e) { if (e instanceof CommandExecutionError && /repeated an earlier response/.test(e.message)) rows = await queryThreads(page, q, { limit: 50 }); else throw e; }

Prevention

When it happens

Trigger: Querying with a limit larger than one PAGE_SIZE when Gmail's next-page navigation fails or re-renders the same results; repeated thread IDs across pages due to a stale page state or a flaky capture of the results list.

Common situations: Large limit values (limit >> PAGE_SIZE) on slow connections; Gmail UI changes breaking the pagination selector so every 'page' shows the same rows; very small result sets interacting with page-advance logic.

Related errors


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