{"record":{"id":"e0092278dd7763c6","repo":"thedotmack/claude-mem","slug":"manual-observation-sources-cannot-be-linked-to-a-g","errorCode":null,"errorMessage":"manual observation sources cannot be linked to a generation_job_id","messagePattern":"manual observation sources cannot be linked to a generation_job_id","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/storage/postgres/observations.ts","lineNumber":332,"sourceCode":"  );\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\n  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');","sourceCodeStart":314,"sourceCodeEnd":350,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/storage/postgres/observations.ts#L314-L350","documentation":"When a generationJobId is supplied, assertGenerationJobMatchesSource rejects sourceType 'manual' outright: a manual observation by definition was not produced by a generation job, so linking one is an invalid state combination.","triggerScenarios":"Calling addSource with sourceType='manual' and a non-null generationJobId, e.g. a generic code path that always forwards generationJobId regardless of source type.","commonSituations":"Bulk importers that stamp generationJobId on every source; UI flows that create manual notes while a generation job context is active; tests reusing an input object across source types.","solutions":["Set generationJobId to null/undefined when sourceType is 'manual'.","If the observation really came from a job, use the job's actual source_type instead of 'manual'.","Strip generationJobId in the caller before dispatching manual sources to addSource."],"exampleFix":"// before\nawait repo.addSource({ sourceType: 'manual', generationJobId: jobId, ... });\n// after\nawait repo.addSource({ sourceType: 'manual', generationJobId: null, ... });","handlingStrategy":"validation","validationCode":"if (sourceType === 'manual' && generationJobId) {\n  throw new Error('strip generationJobId for manual sources before addSource');\n}","typeGuard":"function canLinkGenerationJob(input: { sourceType: string; generationJobId?: string | null }): input is { sourceType: string; generationJobId: string } {\n  return !!input.generationJobId && input.sourceType !== 'manual';\n}","tryCatchPattern":"try {\n  await repo.addSource(input);\n} catch (e) {\n  if (e instanceof Error && e.message.includes('cannot be linked to a generation_job_id')) {\n    await repo.addSource({ ...input, generationJobId: null }); // retry without job link\n  } else throw e;\n}","preventionTips":["Only attach generationJobId for source types produced by jobs (agent_event, session_summary, observation_reindex).","Sanitize input objects once at the pipeline boundary instead of at each call site.","Test each sourceType against addSource with and without generationJobId."],"tags":["postgres","invalid-state","generation-jobs"],"backgroundTag":"invalid-state-transition","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"}