{"record":{"id":"a584ffc9878cc5e6","repo":"thedotmack/claude-mem","slug":"generation-job-id-must-belong-to-project-id-and-te-a584ff","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/observations.ts","lineNumber":316,"sourceCode":"}): string {\n  return `generation:v1:${input.generationJobId}:${input.parsedObservationIndex}:${deterministicKey([\n    canonicalJson(input.content.trim())\n  ])}`;\n}\n\nasync function assertJobOwnership(\n  client: PostgresQueryable,\n  generationJobId: string,\n  projectId: string,\n  teamId: string\n): Promise<void> {\n  const row = await queryOne<{ id: string }>(\n    client,\n    'SELECT id FROM observation_generation_jobs WHERE id = $1 AND project_id = $2 AND team_id = $3',\n    [generationJobId, projectId, teamId]\n  );\n  if (!row) {\n    throw new Error('generation_job_id must belong to project_id and team_id');\n  }\n}\n\nasync function assertGenerationJobMatchesSource(\n  client: PostgresQueryable,\n  input: {\n    generationJobId: string;\n    projectId: string;\n    teamId: string;\n    sourceType: ObservationSourceType;\n    sourceId: string;\n    agentEventId: string | null;\n  }\n): Promise<void> {\n  if (input.sourceType === 'manual') {\n    throw new Error('manual observation sources cannot be linked to a generation_job_id');\n  }\n","sourceCodeStart":298,"sourceCodeEnd":334,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d768ba364302d12b76e69e4f021f0bb1d2d50ed6/src/storage/postgres/observations.ts#L298-L334","documentation":"Thrown by assertJobOwnership (private helper) when PostgresObservationRepository.create is called with a createdByJobId. It looks up observation_generation_jobs by (id, project_id, team_id); no row means the job is not owned by that tenant, so the observation cannot be attributed to it.","triggerScenarios":"Calling observations.create({createdByJobId}) where the job id is absent or belongs to a different project/team. Project/session ownership is checked first; this fires only when createdByJobId is set and unresolvable.","commonSituations":"Recording an observation with a stale job id after the job row changed; cross-tenant job id; passing a bullmq job id rather than the DB job id; creating observations outside the worker that owns the job.","solutions":["Fetch the job with jobs.getByIdForScope before setting createdByJobId.","Omit createdByJobId when the observation is not produced by a tracked job.","Thread the exact job id returned by jobs.create() rather than an external id."],"exampleFix":"// before\nawait observations.create({ projectId, teamId, content, createdByJobId: maybeStaleId });\n// after\nconst job = await jobs.getByIdForScope({ id: createdByJobId, projectId, teamId });\nif (!job) throw new Error('job not in scope');\nawait observations.create({ projectId, teamId, content, createdByJobId: job.id });","handlingStrategy":"validation","validationCode":"if (createdByJobId) { const job = await jobs.getByIdForScope({ id: createdByJobId, projectId, teamId }); if (!job) throw new Error('job not in scope'); }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Only set createdByJobId from the worker that created the job.","Use the DB job id, not the queue id."],"tags":["postgres","validation","multi-tenant","ownership","observations"],"backgroundTag":null,"analyzedSha":"d768ba364302d12b76e69e4f021f0bb1d2d50ed6","analyzedAt":"2026-08-12T23:52:55.241Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}