thedotmack/claude-mem · error

failed to re-enqueue generation job on operator retry

Error message

failed to re-enqueue generation job on operator retry

What it means

The operator retry endpoint updates the job row to queued and then re-publishes it to the BullMQ event queue (a best-effort remove of the old bullmq_job_id, then queue.add with the retry payload). If publishing throws, this warning logs and the row is left queued-but-unpublished: by design the API never claims 'enqueued' when it could not publish, and startup reconciliation publishes such rows on next boot.

Source

Thrown at src/server/routes/v1/ServerV1PostgresRoutes.ts:1567

        requestId: req.requestId ?? null,
        retriedCount,
      },
    });

    // Re-enqueue to BullMQ. If the queue is unavailable we leave the row in
    // queued state and reconciliation will publish it on next startup —
    // never lie about "enqueued" when we couldn't publish.
    const queue = this.resolveEventQueueForRetry(updatedRow as { source_type: string });
    if (queue && updatedRow) {
      try {
        const bullmqJobId = (updatedRow as { bullmq_job_id: string | null }).bullmq_job_id;
        if (bullmqJobId) {
          // Best effort remove first so a terminal-state slot doesn't block.
          try { await queue.remove(bullmqJobId); } catch { /* terminal slot may be missing — ok */ }
          await queue.add(bullmqJobId, retryBullmqPayload as never);
        }
      } catch (error) {
        logger.warn('SYSTEM', 'failed to re-enqueue generation job on operator retry', {
          jobId: id,
          requestId: req.requestId ?? null,
          error: error instanceof Error ? error.message : String(error),
        });
      }
    }

    const refreshed = await repo.getByIdForScope({ id, projectId: current.projectId, teamId });
    if (!refreshed) {
      res.status(404).json({ error: 'NotFound', message: 'Generation job not found' });
      return null;
    }

    await this.auditWrite(req, 'generation_job.retried_by_operator', refreshed.id, refreshed.projectId, {
      previousStatus: current.status,
      currentStatus: refreshed.status,
      retriedCount,
      requestId: req.requestId ?? null,

View on GitHub (pinned to e2d1df569a)

Solutions

  1. Verify Redis from the server host: redis-cli -u "$REDIS_URL" ping
  2. Read the warn's error field: ioredis connection errors point at Redis, serialization errors point at the retry payload shape
  3. Restart the API: startup reconciliation publishes queued-but-unpublished rows, completing the retry
  4. Align queue names, prefixes, and REDIS_URL with the deployment if they drifted

Example fix

// before
await queue.add(bullmqJobId, retryBullmqPayload as never);

// after: retry the publish, then rely on startup reconciliation if it still fails
let published = false;
for (let attempt = 1; attempt <= 3 && !published; attempt++) {
  try {
    await queue.add(bullmqJobId, retryBullmqPayload as never);
    published = true;
  } catch (error) {
    if (attempt === 3) {
      logger.warn('SYSTEM', 'failed to re-enqueue generation job on operator retry', {
        jobId: id,
        error: error instanceof Error ? error.message : String(error),
      });
    } else {
      await new Promise((resolve) => setTimeout(resolve, 100 * attempt));
    }
  }
}
Defensive patterns

Strategy: retry

Validate before calling

// Before triggering operator retries, confirm the queue backend is reachable:
await queue.client.ping(); // the ioredis client BullMQ uses
// If this throws, fix Redis first: retries would only queue rows, not jobs.

Type guard

function isRedisConnectionError(error: unknown): boolean {
  const name = (error as { name?: string } | null)?.name;
  const message = error instanceof Error ? error.message : String(error);
  return name === 'MaxRetriesPerRequestError' || /connection|ECONNREFUSED|ETIMEDOUT/i.test(message);
}

Try / catch

try {
  if (bullmqJobId) {
    try { await queue.remove(bullmqJobId); } catch { /* terminal slot may be missing */ }
    await queue.add(bullmqJobId, retryBullmqPayload as never);
  }
} catch (error) {
  if (isRedisConnectionError(error)) {
    // leave the row queued; startup reconciliation re-publishes it — do not report 'enqueued'
  }
  logger.warn('SYSTEM', 'failed to re-enqueue generation job on operator retry', {
    jobId: id, requestId: req.requestId ?? null, error: error instanceof Error ? error.message : String(error),
  });
}

Prevention

When it happens

Trigger: POSTing an operator retry for a generation job while Redis is down or unreachable (BullMQ add fails), the REDIS_URL or queue name changed between deploys, or the retry payload cannot be serialized for the queue.

Common situations: Redis restart or failover mid-retry; queue prefix or name drift after a config change; Redis maxmemory pressure; network policy blocking the Redis port from new pods.

Related errors


AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20). Data as JSON: /api/errors/57da44f1a3683c5a. Report an issue: GitHub.