thedotmack/claude-mem · error

illegal observation generation job transition from ${current

Error message

illegal observation generation job transition from ${current.status} to ${nextStatus}

What it means

Thrown by assertValidJobStatusTransition when the current status is non-terminal but the requested nextStatus is not in ALLOWED_JOB_TRANSITIONS[current]. The matrix is: queued -> {processing,failed,cancelled}; processing -> {queued,completed,failed,cancelled}; the terminal statuses have no outgoing edges.

Source

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

const ALLOWED_JOB_TRANSITIONS: Record<ObservationGenerationJobStatus, readonly ObservationGenerationJobStatus[]> = {
  queued: ['processing', 'failed', 'cancelled'],
  processing: ['queued', 'completed', 'failed', 'cancelled'],
  completed: [],
  failed: [],
  cancelled: []
};

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,

View on GitHub (pinned to d768ba3643)

Solutions

  1. Route through 'processing' before 'completed' (queued -> processing -> completed).
  2. Audit the requested nextStatus against ALLOWED_JOB_TRANSITIONS before calling transitionStatus.
  3. If you only need to cancel a queued job, use status 'cancelled' (allowed from queued).

Example fix

// before
await jobs.transitionStatus({ id, projectId, teamId, status:'completed' }); // current is queued
// after
await jobs.transitionStatus({ id, projectId, teamId, status:'processing' });
// ... do work ...
await jobs.transitionStatus({ id, projectId, teamId, status:'completed' });
Defensive patterns

Strategy: validation

Validate before calling

const ALLOWED = { queued:['processing','failed','cancelled'], processing:['queued','completed','failed','cancelled'], completed:[], failed:[], cancelled:[] };
function canTransition(from, to) { return ALLOWED[from].includes(to); }
if (!canTransition(current.status, desired)) throw new Error(`unsupported ${current.status}->${desired}`);

Type guard

function isLegalTransition(from, to) { const m={queued:['processing','failed','cancelled'],processing:['queued','completed','failed','cancelled'],completed:[],failed:[],cancelled:[]}; return m[from]?.includes(to) ?? false; }

Prevention

When it happens

Trigger: Examples: transitioning queued -> completed directly (skipping processing), queued -> queued, or processing -> processing. Any transition not explicitly listed is illegal.

Common situations: Worker short-circuiting processing and going straight from queued to completed; calling queued->queued to 'reset'; misordered state updates from concurrent workers.

Related errors


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