{"record":{"id":"2295accf207a5c20","repo":"thedotmack/claude-mem","slug":"internalerror","errorCode":"InternalError","errorMessage":"Failed to persist event","messagePattern":"Failed to persist event","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"src/server/routes/v1/ServerV1PostgresRoutes.ts","lineNumber":1321,"sourceCode":"    const byTeam = await this.options.pool.query(\n      `DELETE FROM observations WHERE id = $1 AND team_id = $2`,\n      [id, teamId],\n    );\n    return (byTeam.rowCount ?? 0) > 0;\n  }\n\n  private handleDbError(error: unknown, res: Response, action: string): void {\n    const message = error instanceof Error ? error.message : String(error);\n    if (\n      message.includes('project_id must belong to team_id')\n      || message.includes('server_session_id must belong')\n      || message.includes('agent_event source_id must belong')\n    ) {\n      res.status(403).json({ error: 'Forbidden', message });\n      return;\n    }\n    logger.error('SYSTEM', `${action} failed`, { error: message });\n    res.status(500).json({ error: 'InternalError', message: 'Failed to persist event' });\n  }\n\n  private async auditWrite(\n    req: Request,\n    action: string,\n    targetId: string | null,\n    projectId: string | null,\n    details?: Record<string, unknown>,\n  ): Promise<void> {\n    const repo = new PostgresAuthRepository(this.options.pool);\n    const actorId = await this.resolveActorId(req);\n    // Phase 12 — every audit row carries request_id when one was minted\n    // so dashboards and incident triage can pivot from a single HTTP\n    // request to every ingest/job/audit row it produced. Caller-supplied\n    // details win on key conflict so explicit overrides still work.\n    const detailsWithRequestId: Record<string, unknown> = {\n      ...(req.requestId ? { requestId: req.requestId } : {}),\n      ...(details ?? {}),","sourceCodeStart":1303,"sourceCodeEnd":1339,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/e2d1df569a8f04075d40e92461128ece7cf04c82/src/server/routes/v1/ServerV1PostgresRoutes.ts#L1303-L1339","documentation":"The fallback branch of handleDbError: any DB error whose message does not match the three team-belongs constraint patterns is logged server-side as '<action> failed' and answered 500 {error:'InternalError', message:'Failed to persist event'}. The generic body hides internals from the client; the real cause exists only in the server log, correlated by requestId.","triggerScenarios":"POST /v1/events, /v1/events/batch, /v1/sessions/start, session end, or project purge hitting a transient Postgres outage, connection-pool exhaustion, a unique-constraint violation, or a serialization failure.","commonSituations":"Database failover mid-deploy; pool maxed out under burst event traffic; a migration adding a NOT NULL column while old clients still submit the legacy shape.","solutions":["Capture the response's requestId and correlate it in server logs to see the true DB error","Retry idempotent writes with exponential backoff (event batches are replayable)","If it persists, check DB connectivity, pool size, and recent schema migrations"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"On 500 'Failed to persist event': capture the requestId from the response/logs, retry with exponential backoff (respect idempotency of the batch), and escalate only after N attempts — the server deliberately hides the underlying DB error from the client.","preventionTips":["Log requestId on every 5xx so server-side root causes are findable","Keep event batches replayable: persist failed batches to a dead-letter queue","Watch pool saturation and DB health metrics when these 500s cluster"],"tags":["http-500","database","persist","internal-error","observability"],"backgroundTag":"database-write-failed","analyzedSha":"e2d1df569a8f04075d40e92461128ece7cf04c82","analyzedAt":"2026-08-20T23:58:13.836Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}