thedotmack/claude-mem · 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

Thrown by PostgresObservationGenerationJobEventsRepository.append. The insert uses INSERT ... SELECT ... FROM observation_generation_jobs WHERE id=$2 AND project_id=$3 AND team_id=$8. If zero rows match, no event row is inserted and the guard throws, meaning the referenced generation job is not owned by the given project and team.

Source

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

        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 d768ba3643)

Solutions

  1. Fetch the job via getByIdForScope first; only append events if it returns non-null.
  2. Re-derive projectId/teamId from the job before appending events rather than trusting caller-supplied scope.
  3. Check whether the job was recreated (idempotency_key collision created a new row) and update the stored generationJobId.

Example fix

// before
await events.append({ generationJobId, projectId, teamId, eventType:'processing', statusAfter:'processing' });
// after
const job = await jobs.getByIdForScope({ id: generationJobId, projectId, teamId });
if (!job) { logger.warn('jobs','job not in scope, skipping event'); return; }
await events.append({ generationJobId, projectId, teamId, eventType:'processing', statusAfter:'processing' });
Defensive patterns

Strategy: validation

Validate before calling

const job = await jobs.getByIdForScope({ id: generationJobId, projectId, teamId });
if (!job) return; // or throw a domain-specific error

Try / catch

try { await events.append({...}); } catch (e) { if (e.message === 'generation_job_id must belong to project_id and team_id') { /* log + drop stale job */ } else throw e; }

Prevention

When it happens

Trigger: Calling events.append() with a generationJobId that does not exist under the supplied projectId/teamId. The job id may be valid in another tenant, deleted, or never created.

Common situations: Worker/queue handler holding a stale job id after the job row was re-created under a new idempotency key; replaying events against the wrong tenant; passing a bullmq job id instead of the DB job id.

Related errors


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