{"record":{"id":"62705560e62eaa79","repo":"thedotmack/claude-mem","slug":"generation-job-id-source-model-must-match-observat","errorCode":null,"errorMessage":"generation_job_id source model must match observation source","messagePattern":"generation_job_id source model must match observation source","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/storage/postgres/observations.ts","lineNumber":353,"sourceCode":"  const row = await queryOne<{\n    id: string;\n    source_type: string;\n    source_id: string;\n    agent_event_id: string | null;\n  }>(\n    client,\n    `\n      SELECT id, source_type, source_id, agent_event_id\n      FROM observation_generation_jobs\n      WHERE id = $1 AND project_id = $2 AND team_id = $3\n    `,\n    [input.generationJobId, input.projectId, input.teamId]\n  );\n  if (!row) {\n    throw new Error('generation_job_id must belong to project_id and team_id');\n  }\n  if (row.source_type !== input.sourceType || row.source_id !== input.sourceId) {\n    throw new Error('generation_job_id source model must match observation source');\n  }\n  if (input.sourceType === 'agent_event' && row.agent_event_id !== input.agentEventId) {\n    throw new Error('generation_job_id agent_event_id must match observation source');\n  }\n}\n\nasync function assertAgentEventOwnership(\n  client: PostgresQueryable,\n  agentEventId: string,\n  projectId: string,\n  teamId: string\n): Promise<void> {\n  const row = await queryOne<{ id: string }>(\n    client,\n    'SELECT id FROM agent_events WHERE id = $1 AND project_id = $2 AND team_id = $3',\n    [agentEventId, projectId, teamId]\n  );\n  if (!row) {","sourceCodeStart":335,"sourceCodeEnd":371,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/storage/postgres/observations.ts#L335-L371","documentation":"After locating the generation job, its stored source_type and source_id are compared with the observation source being linked. Any mismatch means the job did not run for this particular source, so attaching it to the observation would misattribute provenance; the library throws instead.","triggerScenarios":"addSource called with a valid but unrelated generationJobId — the job's (source_type, source_id) differ from the input's sourceType/sourceId, e.g. attaching job X (for event A) to a source record for event B.","commonSituations":"Looping over sources and reusing one job id for all of them; off-by-one pairing of jobs to sources after reordering; copying job ids between rows; retries that lost the original job-to-source mapping.","solutions":["Look up the job and confirm its source_type/source_id columns equal the sourceType/sourceId you pass to addSource.","Pass the generation job id that was actually created for this specific sourceId/sourceType pair.","Fix loop code that assigns a single generationJobId across multiple different sources."],"exampleFix":"// before\nfor (const src of sources) {\n  await repo.addSource({ ...src, generationJobId: firstJobId });\n}\n// after\nfor (const src of sources) {\n  const job = jobsBySource.get(`${src.sourceType}:${src.sourceId}`);\n  await repo.addSource({ ...src, generationJobId: job?.id ?? null });\n}","handlingStrategy":"validation","validationCode":"const job = await db.one(\n  'SELECT source_type, source_id FROM observation_generation_jobs WHERE id = $1', [generationJobId]\n);\nif (job.source_type !== sourceType || job.source_id !== sourceId) {\n  throw new Error(`job ${generationJobId} does not match ${sourceType}:${sourceId}`);\n}","typeGuard":"function jobMatchesSource(job: { source_type: string; source_id: string }, src: { sourceType: string; sourceId: string }): boolean {\n  return job.source_type === src.sourceType && job.source_id === src.sourceId;\n}","tryCatchPattern":"try {\n  await repo.addSource(input);\n} catch (e) {\n  if (e instanceof Error && e.message === 'generation_job_id source model must match observation source') {\n    logger.warn('job/source pairing mismatch', { jobId: input.generationJobId, src: input.sourceId });\n    return null;\n  }\n  throw e;\n}","preventionTips":["Keep a Map from `${sourceType}:${sourceId}` to its generation job id instead of reusing one id.","Pair jobs to sources by key, never by loop index.","Add an assertion in the generation pipeline that each job is consumed exactly once for its own source."],"tags":["postgres","provenance","mismatch"],"backgroundTag":"invalid-argument-value","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"}