{"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":"Error","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/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/storage/postgres/observations.ts#L298-L334","documentation":"assertJobOwnership looks up observation_generation_jobs by id scoped to project_id and team_id. If no row matches, the generation job id passed to create does not exist in that tenant scope, so the observation creation is rejected. This ensures observations can only reference generation jobs owned by the same project and team.","triggerScenarios":"Passing a generationJobId to the observations create path where the job id is wrong, belongs to another project/team, or the job row was purged before observations were created.","commonSituations":"Replaying persisted job payloads against a different database/environment; multi-tenant mixups where projectId/teamId come from a different request context; retention cleanup deleted jobs while observations were still being generated.","solutions":["Query observation_generation_jobs with the id plus exact projectId/teamId to confirm the job exists.","Ensure projectId/teamId passed to create come from the same tenant context that created the job.","If the job was deleted, re-create the generation job before creating observations that reference it.","Remove generationJobId if the observation was not actually produced by a generation job."],"exampleFix":"// before\nawait repo.create({ ..., generationJobId: job.id, projectId: otherProjectId });\n// after\nconst job = await jobs.get({ id: jobId, projectId, teamId });\nif (!job) throw new Error(`generation job ${jobId} not found in project ${projectId}`);\nawait repo.create({ ..., generationJobId: job.id, projectId, teamId });","handlingStrategy":"try-catch","validationCode":"const job = await db.oneOrNone(\n  'SELECT id FROM observation_generation_jobs WHERE id = $1 AND project_id = $2 AND team_id = $3',\n  [generationJobId, projectId, teamId]\n);\nif (!job) return; // skip creating an observation tied to a missing job","typeGuard":null,"tryCatchPattern":"try {\n  await repo.create(input);\n} catch (e) {\n  if (e instanceof Error && e.message === 'generation_job_id must belong to project_id and team_id') {\n    logger.error('generation job out of scope', { generationJobId: input.generationJobId, projectId, teamId });\n    return null;\n  }\n  throw e;\n}","preventionTips":["Create observations and their generation job in one transaction.","Thread projectId/teamId through the pipeline from one request context.","Retention/purge jobs should not delete generation jobs still referenced by in-flight pipelines."],"tags":["postgres","tenant-scope","generation-jobs"],"backgroundTag":"record-not-found","analyzedSha":"d8bc9755e74915e5c3b999181e10a67c889bce2a","analyzedAt":"2026-09-17T16:40:26.182Z","contentChangedAt":"2026-09-17T16:40:26.182Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}