santifer/career-ops · warning

gmail: could not persist processed-id state — ${err.message}

Error message

gmail: could not persist processed-id state — ${err.message}

What it means

saveProcessedIds in plugins/gmail/index.mjs persists the set of already-processed Gmail message IDs to STATE_PATH under data/. If mkdirSync or writeFileSync throws, it logs 'gmail: could not persist processed-id state — <cause>' via console.warn and continues. The consequence is not immediate failure but lost dedup state: on the next run the same messages will be reprocessed (duplicate ingests).

Source

Thrown at plugins/gmail/index.mjs:66

  return data.access_token;
}

function loadProcessedIds() {
  if (!existsSync(STATE_PATH)) return new Set();
  try {
    const state = JSON.parse(readFileSync(STATE_PATH, 'utf-8'));
    return new Set(state.processed_message_ids || []);
  } catch {
    return new Set();
  }
}

function saveProcessedIds(ids) {
  try {
    mkdirSync('data', { recursive: true });
    writeFileSync(STATE_PATH, JSON.stringify({ processed_message_ids: [...ids] }, null, 2), 'utf-8');
  } catch (err) {
    console.warn(`gmail: could not persist processed-id state — ${err.message}`);
  }
}

/** @type {{ ingest: (ctx: any) => Promise<object[]> }} */
export default {
  async ingest(ctx) {
    const clientId = ctx?.env?.GMAIL_CLIENT_ID;
    const clientSecret = ctx?.env?.GMAIL_CLIENT_SECRET;
    const refreshToken = ctx?.env?.GMAIL_REFRESH_TOKEN;
    if (!clientId || !clientSecret || !refreshToken) {
      throw new Error('gmail: missing GMAIL_CLIENT_ID / GMAIL_CLIENT_SECRET / GMAIL_REFRESH_TOKEN in .env');
    }

    const label = ctx?.settings?.label || 'Job Leads';
    const daysBack = Number(ctx?.settings?.days_back ?? 7);
    if (!Number.isInteger(daysBack) || daysBack <= 0) {
      throw new Error(`gmail: invalid days_back "${ctx?.settings?.days_back}" (must be a positive integer)`);
    }

View on GitHub (pinned to 1696bec4d0)

Solutions

  1. Check err.message for the errno code: EACCES/EROFS → run from a writable directory or fix filesystem permissions; ENOSPC → free disk space
  2. Run the plugin from the repository root so the relative data/ path resolves to the project's writable data directory
  3. After fixing persistence, re-run ingest — previously processed messages will be reprocessed once, then dedup resumes
  4. If persistence is genuinely impossible in the environment, capture the returned items yourself and dedup downstream, since the plugin will silently re-ingest on every run

Example fix

// before
mkdirSync('data', { recursive: true });
writeFileSync(STATE_PATH, JSON.stringify({ processed_message_ids: [...ids] }, null, 2), 'utf-8');
// EACCES when cwd is read-only
// after: resolve state relative to the project root
const ROOT = path.resolve(import.meta.dirname, '..', '..');
mkdirSync(path.join(ROOT, 'data'), { recursive: true });
writeFileSync(path.join(ROOT, 'data', 'gmail-state.json'), JSON.stringify({ processed_message_ids: [...ids] }, null, 2), 'utf-8');
Defensive patterns

Strategy: fallback

Validate before calling

import { existsSync, accessSync, constants } from 'fs';
function statePathWritable(p) {
  const dir = path.dirname(p);
  if (!existsSync(dir)) return false;
  try { accessSync(dir, constants.W_OK); return true; } catch { return false; }
}
// if false, expect the warning and re-ingestion of old messages

Try / catch

const items = await gmailPlugin.ingest(ctx);
// persistence is best-effort; detect lost state by comparing with a previously saved snapshot
const prevIds = loadExternalSnapshot()?.processed_message_ids ?? [];
const lostState = prevIds.length > 0 && items.length > prevIds.length;
if (lostState) deduplicate(items, prevIds);

Prevention

When it happens

Trigger: Calling saveProcessedIds when the data/ directory cannot be created (EACCES/EPERM on the cwd, read-only filesystem EROFS), writeFileSync fails due to ENOSPC (disk full), or STATE_PATH resolves to an invalid path (e.g. running from a directory where 'data' cannot be created).

Common situations: Running the gmail plugin from a read-only checkout or a CI step whose workspace is mounted read-only; disk quota exceeded; executing from a cwd where creating data/ isn't permitted; STATE_PATH pointing outside the project because the plugin was invoked from another directory.

Understand the failure class

Background: "failed to write file", "Could not save figure", "Error saving remote file" — file write failed: causes and fixes across languages and libraries — this error's family across 38 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@1696bec4d0 (2026-09-01). Data as JSON: /api/errors/00d469ee16d91e89. Report an issue: GitHub.