thedotmack/claude-mem · error

cannot retry observation generation job after max_attempts i

Error message

cannot retry observation generation job after max_attempts is reached

What it means

Thrown by assertValidJobStatusTransition when nextStatus is 'queued' (i.e. a retry/back-to-queue) and current.attempts >= current.maxAttempts. Re-queueing for another attempt is not allowed once the budget is spent. Only reachable from 'processing' -> 'queued'.

Source

Thrown at src/storage/postgres/generation-jobs.ts:415

function assertValidJobStatusTransition(
  current: PostgresObservationGenerationJob,
  nextStatus: ObservationGenerationJobStatus
): void {
  if (TERMINAL_JOB_STATUSES.has(current.status)) {
    throw new Error(`cannot transition observation generation job from terminal status ${current.status}`);
  }

  if (!ALLOWED_JOB_TRANSITIONS[current.status].includes(nextStatus)) {
    throw new Error(`illegal observation generation job transition from ${current.status} to ${nextStatus}`);
  }

  if (nextStatus === 'processing' && current.attempts >= current.maxAttempts) {
    throw new Error('cannot process observation generation job after max_attempts is reached');
  }

  if (nextStatus === 'queued' && current.attempts >= current.maxAttempts) {
    throw new Error('cannot retry observation generation job after max_attempts is reached');
  }
}

function mapJobRow(row: JobRow): PostgresObservationGenerationJob {
  return {
    id: row.id,
    projectId: row.project_id,
    teamId: row.team_id,
    agentEventId: row.agent_event_id,
    sourceType: row.source_type,
    sourceId: row.source_id,
    serverSessionId: row.server_session_id,
    jobType: row.job_type,
    status: row.status,
    idempotencyKey: row.idempotency_key,
    bullmqJobId: row.bullmq_job_id,
    attempts: row.attempts,
    maxAttempts: row.max_attempts,

View on GitHub (pinned to d768ba3643)

Solutions

  1. Check attempts < maxAttempts before scheduling a retry (transition to 'queued').
  2. Transition to 'failed' with lastError once the budget is exhausted.
  3. Raise maxAttempts on a new job if more attempts are truly required.

Example fix

// before
await jobs.transitionStatus({ id, projectId, teamId, status:'queued', nextAttemptAt: backoffDate });
// after
if (job.attempts >= job.maxAttempts) {
  await jobs.transitionStatus({ id, projectId, teamId, status:'failed', lastError });
} else {
  await jobs.transitionStatus({ id, projectId, teamId, status:'queued', nextAttemptAt: backoffDate });
}
Defensive patterns

Strategy: validation

Validate before calling

if (job.attempts >= job.maxAttempts) { await jobs.transitionStatus({ id, projectId, teamId, status:'failed', lastError }); return; }

Type guard

function canRetry(job){ return job.attempts < job.maxAttempts; }

Prevention

When it happens

Trigger: Calling transitionStatus({status:'queued'}) on a processing job that already used all attempts (a retry-schedule path). The DB UPDATE blocks it and this guard explains why.

Common situations: Backoff scheduler naively re-queuing every failed attempt; transient error handler scheduling retries without checking remaining budget; concurrent retry schedulers.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d768ba3643 (2026-08-12). Data as JSON: /api/errors/a023ce5aafeded3b. Report an issue: GitHub.