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
- Check err.message for the errno code: EACCES/EROFS → run from a writable directory or fix filesystem permissions; ENOSPC → free disk space
- Run the plugin from the repository root so the relative data/ path resolves to the project's writable data directory
- After fixing persistence, re-run ingest — previously processed messages will be reprocessed once, then dedup resumes
- 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
- Run the plugin from a writable working directory (repo root)
- Mirror the processed-id state to an external store (git-tracked file, object storage) so dedup survives local write failures
- Check err.message on the warning and fix permissions/disk before the next scheduled run
- Keep an eye on disk space in long-running environments
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
- ⚠️ Could not save report: ${err.message}
- ⚠️ Keep #${a.num} and #${b.num}: exact-title match but adva
- ⚠️ Additions carry a URL but this tracker has no URL column
- Skipped #${r.num}: row no longer exists in the tracker
- cannot read ${rel || '.'}: ${err.message}
AI-assisted analysis of santifer/career-ops@1696bec4d0 (2026-09-01).
Data as JSON: /api/errors/00d469ee16d91e89.
Report an issue: GitHub.