{"record":{"id":"63f9ebe2c8e01e71","repo":"thedotmack/claude-mem","slug":"generation-job-id-must-belong-to-project-id-and-te","errorCode":null,"errorMessage":"generation_job_id must belong to project_id and team_id","messagePattern":"generation_job_id must belong to project_id and team_id","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/storage/postgres/generation-jobs.ts","lineNumber":333,"sourceCode":"        FROM observation_generation_jobs jobs\n        WHERE jobs.id = $2\n          AND jobs.project_id = $3\n          AND jobs.team_id = $8\n        RETURNING observation_generation_job_events.*\n      `,\n      [\n        input.id ?? newId(),\n        input.generationJobId,\n        input.projectId,\n        input.eventType,\n        input.statusAfter,\n        input.attempt ?? 0,\n        JSON.stringify(input.details ?? {}),\n        input.teamId\n      ]\n    );\n    if (!row) {\n      throw new Error('generation_job_id must belong to project_id and team_id');\n    }\n    return mapJobEventRow(row!);\n  }\n\n  async listByJobForScope(input: {\n    generationJobId: string;\n    projectId: string;\n    teamId: string;\n  }): Promise<PostgresObservationGenerationJobEvent[]> {\n    const result = await this.client.query<JobEventRow>(\n      `\n        SELECT events.*\n        FROM observation_generation_job_events events\n        INNER JOIN observation_generation_jobs jobs ON jobs.id = events.generation_job_id\n        WHERE events.generation_job_id = $1 AND jobs.project_id = $2 AND jobs.team_id = $3\n        ORDER BY events.created_at ASC\n      `,\n      [input.generationJobId, input.projectId, input.teamId]","sourceCodeStart":315,"sourceCodeEnd":351,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d768ba364302d12b76e69e4f021f0bb1d2d50ed6/src/storage/postgres/generation-jobs.ts#L315-L351","documentation":"Thrown by PostgresObservationGenerationJobEventsRepository.append. The insert uses INSERT ... SELECT ... FROM observation_generation_jobs WHERE id=$2 AND project_id=$3 AND team_id=$8. If zero rows match, no event row is inserted and the guard throws, meaning the referenced generation job is not owned by the given project and team.","triggerScenarios":"Calling events.append() with a generationJobId that does not exist under the supplied projectId/teamId. The job id may be valid in another tenant, deleted, or never created.","commonSituations":"Worker/queue handler holding a stale job id after the job row was re-created under a new idempotency key; replaying events against the wrong tenant; passing a bullmq job id instead of the DB job id.","solutions":["Fetch the job via getByIdForScope first; only append events if it returns non-null.","Re-derive projectId/teamId from the job before appending events rather than trusting caller-supplied scope.","Check whether the job was recreated (idempotency_key collision created a new row) and update the stored generationJobId."],"exampleFix":"// before\nawait events.append({ generationJobId, projectId, teamId, eventType:'processing', statusAfter:'processing' });\n// after\nconst job = await jobs.getByIdForScope({ id: generationJobId, projectId, teamId });\nif (!job) { logger.warn('jobs','job not in scope, skipping event'); return; }\nawait events.append({ generationJobId, projectId, teamId, eventType:'processing', statusAfter:'processing' });","handlingStrategy":"validation","validationCode":"const job = await jobs.getByIdForScope({ id: generationJobId, projectId, teamId });\nif (!job) return; // or throw a domain-specific error","typeGuard":null,"tryCatchPattern":"try { await events.append({...}); } catch (e) { if (e.message === 'generation_job_id must belong to project_id and team_id') { /* log + drop stale job */ } else throw e; }","preventionTips":["Always scope job lookups before emitting events.","Discard queued events for jobs that no longer exist rather than failing the worker."],"tags":["postgres","validation","multi-tenant","generation-jobs","events"],"backgroundTag":null,"analyzedSha":"d768ba364302d12b76e69e4f021f0bb1d2d50ed6","analyzedAt":"2026-08-12T23:52:55.241Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}