{"record":{"id":"5d7816521666a72b","repo":"thedotmack/claude-mem","slug":"observation-generation-job-status-transition-was-n","errorCode":null,"errorMessage":"observation generation job status transition was not applied","messagePattern":"observation generation job status transition was not applied","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/storage/postgres/generation-jobs.ts","lineNumber":223,"sourceCode":"        input.lastError == null ? null : JSON.stringify(input.lastError),\n        input.projectId,\n        input.teamId\n      ]\n    );\n    if (row) {\n      return mapJobRow(row);\n    }\n\n    const current = await queryOne<JobRow>(\n      this.client,\n      'SELECT * FROM observation_generation_jobs WHERE id = $1 AND project_id = $2 AND team_id = $3',\n      [input.id, input.projectId, input.teamId]\n    );\n    if (!current) {\n      return null;\n    }\n    assertValidJobStatusTransition(mapJobRow(current), input.status);\n    throw new Error('observation generation job status transition was not applied');\n  }\n\n  async listByStatusForScope(input: {\n    status: ObservationGenerationJobStatus;\n    projectId: string;\n    teamId: string;\n    limit?: number;\n  }): Promise<PostgresObservationGenerationJob[]> {\n    const result = await this.client.query<JobRow>(\n      `\n        SELECT * FROM observation_generation_jobs\n        WHERE status = $1 AND project_id = $2 AND team_id = $3\n        ORDER BY created_at ASC\n        LIMIT $4\n      `,\n      [input.status, input.projectId, input.teamId, input.limit ?? 100]\n    );\n    return result.rows.map(mapJobRow);","sourceCodeStart":205,"sourceCodeEnd":241,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d768ba364302d12b76e69e4f021f0bb1d2d50ed6/src/storage/postgres/generation-jobs.ts#L205-L241","documentation":"Thrown by transitionStatus when the UPDATE ... RETURNING matched zero rows, yet the job row exists (current found) and assertValidJobStatusTransition did not reject. The UPDATE's WHERE clause enforces extra guards the validator doesn't: status must be 'queued' or 'processing', the target must be an allowed transition, AND attempts < max_attempts. So a logically-valid transition that the DB rejected (typically because the retry budget is exhausted: attempts >= max_attempts) hits this 'should-not-happen-per-validator' branch.","triggerScenarios":"transitionStatus called to move a job to 'processing' or 'queued' when the job has already consumed attempts >= max_attempts, or its current status isn't 'queued'/'processing' (e.g., already completed/cancelled). The validator passes but the guarded UPDATE matches nothing.","commonSituations":"Retrying a job that exceeded max_attempts; concurrent workers racing on the same job where one already moved it out of queued/processing; caller requesting a transition from a terminal status (completed/failed/cancelled) which the validator allows but the DB guard rejects.","solutions":["Check the job's attempts vs max_attempts before transitioning; stop retrying once exhausted.","Ensure you only transition jobs currently in 'queued' or 'processing' (re-fetch status before transitioning).","Guard against concurrent transitions with locking/lockedBy so two workers don't both act on a stale status.","If the validator is more permissive than the DB guard (a bug), tighten assertValidJobStatusTransition to match the WHERE clause so callers get a clearer error."],"exampleFix":"// before\nawait repo.transitionStatus({ id, projectId, teamId, status: 'processing' });\n\n// after\nconst job = await repo.findById(id, projectId, teamId);\nif (!job || job.attempts >= job.maxAttempts) {\n  throw new Error('Job retry budget exhausted');\n}\nif (job.status !== 'queued' && job.status !== 'processing') {\n  throw new Error(`Cannot transition from terminal status ${job.status}`);\n}\nawait repo.transitionStatus({ id, projectId, teamId, status: 'processing' });","handlingStrategy":"validation","validationCode":"// Re-fetch and check DB-level guards before transitioning\nconst job = await repo.findById(input.id, input.projectId, input.teamId);\nif (!job) throw new Error('Job not found');\nif (job.attempts >= job.maxAttempts) throw new Error('Retry budget exhausted');\nif (job.status !== 'queued' && job.status !== 'processing') {\n  throw new Error(`Cannot transition from ${job.status}`);\n}\nawait repo.transitionStatus(input);","typeGuard":null,"tryCatchPattern":"try {\n  const updated = await repo.transitionStatus(input);\n  if (!updated) throw new Error('Transition not applied');\n} catch (err) {\n  if (err instanceof Error && /status transition was not applied/.test(err.message)) {\n    // job likely terminal or budget exhausted; re-fetch and handle accordingly\n    const current = await repo.findById(input.id, input.projectId, input.teamId);\n    logger.warn('JOBS', 'Transition rejected by DB guard', { current });\n  }\n  throw err;\n}","preventionTips":["Check attempts < max_attempts and current status (queued/processing) before transitioning.","Use lockedBy/locking to prevent concurrent workers racing on one job.","Align assertValidJobStatusTransition with the UPDATE WHERE guards so callers get the right error."],"tags":["postgres","jobs","state-machine","concurrency","database"],"backgroundTag":null,"analyzedSha":"d768ba364302d12b76e69e4f021f0bb1d2d50ed6","analyzedAt":"2026-08-12T23:52:55.241Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}