thedotmack/claude-mem · error
storeObservations: ON CONFLICT without existing row for…
Error message
storeObservations: ON CONFLICT without existing row for content_hash=${contentHash} What it means
storeObservations performs an INSERT ... ON CONFLICT(content_hash) DO UPDATE/DO NOTHING style upsert and then, for rows that conflicted, re-looks-up the existing row by (memorySessionId, contentHash) to collect its id. If the conflict path was taken but the lookup finds no row, an internal invariant of the upsert is broken, so it throws.
Solutions
- Retry the whole storeObservations call; the transient race usually resolves on a second attempt.
- Serialize writes with a single writer connection/busy_timeout so concurrent upserts cannot interleave.
- Verify the (memory_session_id, content_hash) unique index still exists and matches the ON CONFLICT clause.
- Check for code that deletes observation rows concurrently (pruning/cleanup jobs) and exclude the session being written.
Defensive patterns
Strategy: retry
Try / catch
try {
ids = store.storeObservations(sessionId, project, observations);
} catch (e) {
if (String(e.message).includes('ON CONFLICT without existing row')) {
ids = retryWithBackoff(() => store.storeObservations(sessionId, project, observations));
} else throw e;
} Prevention
- Use a single writer process/connection for observation writes per database.
- Enable SQLite busy_timeout and WAL mode to reduce concurrent-write races.
- Never mutate the (memory_session_id, content_hash) unique index without migrating the ON CONFLICT clause.
When it happens
Trigger: Concurrent writers inserting the same content_hash into the same memory session between the conflicting INSERT and the lookupExistingStmt.get() re-read, or a deleted row / wrong session id making the lookup miss, or a manually corrupted dedupe index.
Common situations: Two processes (e.g. two Claude-mem workers or a CLI plus the plugin) writing identical observations simultaneously; a UNIQUE index that was changed or dropped by a migration; passing a memorySessionId whose rows were pruned mid-write.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- agent_event source_id must equal agent_event_id
- canonical content
- canonical content: exceeds the local SQLite safe-integer…
- cloud sync canonical payload
- cloud sync canonical payload
AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17).
Data as JSON: /api/errors/997ae735b341ba75.
Report an issue: GitHub.
Appendix: source
Thrown at src/services/sqlite/SessionStore.ts:3194
promptNumber || null,
discoveryTokens,
observation.agent_type ?? null,
observation.agent_id ?? null,
contentHash,
timestampIso,
timestampEpoch,
generatedByModel || null,
observation.metadata ?? null
) as { id: number } | null;
if (inserted) {
observationIds.push(inserted.id);
continue;
}
const existing = lookupExistingStmt.get(memorySessionId, contentHash) as { id: number } | null;
if (!existing) {
throw new Error(
`storeObservations: ON CONFLICT without existing row for content_hash=${contentHash}`
);
}
observationIds.push(existing.id);
}
let summaryId: number | null = null;
if (summary) {
const rolledUp = rollupObservationFileLists(observations);
const filesRead = summary.files_read ?? rolledUp.files_read;
const filesEdited = summary.files_edited ?? rolledUp.files_edited;
const summaryStmt = this.db.prepare(`
INSERT INTO session_summaries
(memory_session_id, project, request, investigated, learned, completed,
next_steps, files_read, files_edited, notes, prompt_number, discovery_tokens, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
View on GitHub (pinned to d8bc9755e7)