thedotmack/claude-mem · error · Error

generation_job_id must belong to project_id and team_id

Error message

generation_job_id must belong to project_id and team_id

What it means

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.

Solutions

  1. Query observation_generation_jobs with the id plus exact projectId/teamId to confirm the job exists.
  2. Ensure projectId/teamId passed to create come from the same tenant context that created the job.
  3. If the job was deleted, re-create the generation job before creating observations that reference it.
  4. Remove generationJobId if the observation was not actually produced by a generation job.

Example fix

// before
await repo.create({ ..., generationJobId: job.id, projectId: otherProjectId });
// after
const job = await jobs.get({ id: jobId, projectId, teamId });
if (!job) throw new Error(`generation job ${jobId} not found in project ${projectId}`);
await repo.create({ ..., generationJobId: job.id, projectId, teamId });
Defensive patterns

Strategy: try-catch

Validate before calling

const job = await db.oneOrNone(
  'SELECT id FROM observation_generation_jobs WHERE id = $1 AND project_id = $2 AND team_id = $3',
  [generationJobId, projectId, teamId]
);
if (!job) return; // skip creating an observation tied to a missing job

Try / catch

try {
  await repo.create(input);
} catch (e) {
  if (e instanceof Error && e.message === 'generation_job_id must belong to project_id and team_id') {
    logger.error('generation job out of scope', { generationJobId: input.generationJobId, projectId, teamId });
    return null;
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17). Data as JSON: /api/errors/a584ffc9878cc5e6. Report an issue: GitHub.

Appendix: source

Thrown at src/storage/postgres/observations.ts:316

}): string {
  return `generation:v1:${input.generationJobId}:${input.parsedObservationIndex}:${deterministicKey([
    canonicalJson(input.content.trim())
  ])}`;
}

async function assertJobOwnership(
  client: PostgresQueryable,
  generationJobId: string,
  projectId: string,
  teamId: string
): Promise<void> {
  const row = await queryOne<{ id: string }>(
    client,
    'SELECT id FROM observation_generation_jobs WHERE id = $1 AND project_id = $2 AND team_id = $3',
    [generationJobId, projectId, teamId]
  );
  if (!row) {
    throw new Error('generation_job_id must belong to project_id and team_id');
  }
}

async function assertGenerationJobMatchesSource(
  client: PostgresQueryable,
  input: {
    generationJobId: string;
    projectId: string;
    teamId: string;
    sourceType: ObservationSourceType;
    sourceId: string;
    agentEventId: string | null;
  }
): Promise<void> {
  if (input.sourceType === 'manual') {
    throw new Error('manual observation sources cannot be linked to a generation_job_id');
  }

View on GitHub (pinned to d8bc9755e7)