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

append() on the job-events repository inserts a job event but only succeeds if the generation_job_id belongs to the given project_id and team_id; if the INSERT ... RETURNING matches no row, it throws. This prevents appending events to jobs outside the caller's scope.

Solutions

  1. Confirm the generation job exists under the same project_id/team_id before appending events
  2. Refetch the job by id scoped to your project/team and use that row's id
  3. Check for concurrent cancellation/deletion of the job and handle the not-found case gracefully
  4. Verify you are not passing a different entity's id (observation/event) as generationJobId

Example fix

// before
await jobEvents.append({ generationJobId: someId, projectId, teamId, type: 'note' });
// after
const job = await jobs.getByIdForScope(someId, projectId, teamId);
if (!job) throw new Error('job not in scope');
await jobEvents.append({ generationJobId: job.id, projectId, teamId, type: 'note' });
Defensive patterns

Strategy: validation

Validate before calling

const job = await client.query('SELECT 1 FROM observation_generation_jobs WHERE id=$1 AND project_id=$2 AND team_id=$3', [jobId, projectId, teamId]);
if (job.rowCount === 0) throw new Error('generation job not in scope');

Try / catch

try {
  await jobEvents.append(input);
} catch (err) {
  if (err instanceof Error && err.message.includes('generation_job_id must belong')) {
    logger.warn({ jobId: input.generationJobId }, 'job missing or out of scope; skipping event append');
    return null;
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling append() (or markGenerationFailed / persistGeneratedObservations / retryGenerationJob / cancelGenerationJob / txResult / txResults) with a generationJobId that does not exist, or exists under a different project_id/team_id than the input scope.

Common situations: Worker processing a job whose row was concurrently deleted or cancelled; cross-tenant id reuse after data deletion; passing the observation id instead of the job id; stale job id from a previous database reset.

Understand the failure class

Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.

Related errors


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

Appendix: source

Thrown at src/storage/postgres/generation-jobs.ts:342

        FROM observation_generation_jobs jobs
        WHERE jobs.id = $2
          AND jobs.project_id = $3
          AND jobs.team_id = $8
        RETURNING observation_generation_job_events.*
      `,
      [
        input.id ?? newId(),
        input.generationJobId,
        input.projectId,
        input.eventType,
        input.statusAfter,
        input.attempt ?? 0,
        JSON.stringify(input.details ?? {}),
        input.teamId
      ]
    );
    if (!row) {
      throw new Error('generation_job_id must belong to project_id and team_id');
    }
    return mapJobEventRow(row!);
  }

  async listByJobForScope(input: {
    generationJobId: string;
    projectId: string;
    teamId: string;
  }): Promise<PostgresObservationGenerationJobEvent[]> {
    const result = await this.client.query<JobEventRow>(
      `
        SELECT events.*
        FROM observation_generation_job_events events
        INNER JOIN observation_generation_jobs jobs ON jobs.id = events.generation_job_id
        WHERE events.generation_job_id = $1 AND jobs.project_id = $2 AND jobs.team_id = $3
        ORDER BY events.created_at ASC
      `,
      [input.generationJobId, input.projectId, input.teamId]

View on GitHub (pinned to d8bc9755e7)