---
title: "A Session ID Bug Turned Claude-Mem Restarts Into Millions of Queue Rows"
description: "Claude-Mem minted a new synthetic session ID after certain generator restarts, then requeued every prompt in the conversation. The upstream fix now reuses durable identity and skips unchanged updates."
canonical: "https://www.symbaiex.com/blog/claude-mem-session-id-churn-sync-outbox-fix"
last-updated: "2026-09-13T00:00:00.000Z"
---
# A Session ID Bug Turned Claude-Mem Restarts Into Millions of Queue Rows

> Claude-Mem minted a new synthetic session ID after certain generator restarts, then requeued every prompt in the conversation. The upstream fix now reuses durable identity and skips unchanged updates.

Published: 2026-09-13  
Reading time: 5 minutes

Tags: claude-mem, open source, ai agents, sqlite, reliability

Claude-Mem looked healthy while its local sync queue kept growing. The worker was running, generation still worked, and there was no obvious crash loop. Underneath that normal surface, one local database had accumulated roughly 3.9 million pending `set_prompt_session` operations. Two consecutive reads added another 163 rows.

The problem was not the volume of new memories. A session reload was changing an identifier that should have stayed stable. That one identity change made Claude-Mem treat every existing prompt as if it needed to be repaired and synchronized again.

I traced the behavior, built the fix, and opened [PR #3597](https://github.com/thedotmack/claude-mem/pull/3597). The maintainer later rehosted the patch on current main as [PR #4032](https://github.com/thedotmack/claude-mem/pull/4032), preserved my co-author credit, and merged it on September 11. The change shipped in [claude-mem v13.24.18](https://github.com/thedotmack/claude-mem/releases/tag/v13.24.18).

Here is what was happening and why the final patch is deliberately small.

## The worker looked healthy while the database kept growing

The affected path used an OpenAI-compatible generator such as OpenRouter or Gemini. Claude-Mem keeps a content session for the conversation and a memory-session ID for the generator work attached to it.

When an in-memory session is reconstructed, Claude-Mem intentionally does not carry over a remote Claude SDK session ID. That protects against resuming a stale remote session. OpenAI-compatible providers do not use that remote Claude session. They use a local synthetic ID instead.

The bug was that the OpenAI-compatible provider created a fresh synthetic ID with the current timestamp whenever the in-memory session came back without one. The conversation had not changed. The provider had not changed. The durable database record still knew the old synthetic ID. The runtime ignored that record and invented a new identity anyway.

![Sequence diagram showing a session reload minting a new synthetic ID, requeueing every prompt, and multiplying sync work across restarts.](https://strong-bee-384.convex.cloud/api/storage/db9f920e-810c-4105-989a-5a460b91be4a)

*Image: SYMBiEX editorial system*

Every newly minted ID then called `updateMemorySessionId`. That method updated the session and requeued `set_prompt_session` for every native prompt associated with it. A long conversation made each restart expensive. More restarts repeated the same work.

The amplification followed a simple shape: restarts multiplied by prompts already in the conversation. Nothing had to fail loudly for the queue to balloon.

## The patch stabilizes identity in two places

The provider now builds the expected prefix from the generator and content-session identity, then reads the persisted `memory_session_id`. If the stored value belongs to the same provider and content session, it is reused. A new synthetic ID is created only when the stored value is missing or belongs to a different provider or session.

That preserves the safety boundary. Switching providers or moving to a different content session still creates a new identity. Reconstructing the same session no longer does.

The storage method also gained an idempotency check. `updateMemorySessionId` reads the current value and returns immediately when the requested ID is already stored. Even if another caller repeats the update, it cannot trigger another prompt-repair pass for an unchanged value.

![Decision matrix showing when Claude-Mem reuses a persisted synthetic session ID and when it creates a new one.](https://strong-bee-384.convex.cloud/api/storage/f2f7c800-57dd-44ae-87bf-2aed04725368)

*Image: SYMBiEX editorial system*

These checks cover different failure modes. The provider owns the decision about whether a synthetic identity is still valid. The store owns the decision about whether a mutation changes durable state. Keeping both checks makes the behavior easier to reason about and harder to regress.

## The active workload made the result obvious

The patch added focused tests for three cases:

- reuse a persisted synthetic ID when the in-memory session restarts
- replace a persisted ID when it belongs to another provider
- emit no new sync work when `updateMemorySessionId` receives the existing value

The local verification also passed the full typecheck and test suite: 2,560 tests passed, 18 were skipped, and none failed.

The active workload was the more important check. Before the patch, repeated reads kept adding `set_prompt_session` rows. After the stale backlog was cleared and the patched worker restarted, the logs showed `MEMORY_ID_REUSED` and the outbox stayed at zero across repeated restarts. Generation continued to work.

That distinction matters. A green unit test proves the intended branch. A live persistence check proves the branch stopped producing the expensive side effect that started the investigation.

## How the change landed upstream

The original contribution was [PR #3597](https://github.com/thedotmack/claude-mem/pull/3597), opened from my fork under the SYMBaiEX account. By the time it was reviewed, the branch was well behind current main. The maintainer rehosted the same change as [PR #4032](https://github.com/thedotmack/claude-mem/pull/4032), named the original PR and author in the description, and included the co-author line in the merged commit.

That detail is worth recording accurately. PR #3597 was closed as superseded. The patch itself landed through #4032 and became part of the v13.24.18 release.

The merged diff remained compact: 63 additions, four deletions, and four files. Most of that change was regression coverage. The runtime behavior changed in two methods.

## Stable identity belongs in durable state

This bug is specific to Claude-Mem, but the design problem appears in many agent systems. A process restart is not automatically a new logical session. When runtime state is rebuilt from durable storage, identities that survive the restart should come from that durable record. A timestamp is useful for creating a genuinely new identity. It is a poor default for recovering an existing one.

Mutation boundaries also need to defend themselves. Callers will retry. Processes will restart. Events will be delivered more than once. If an update can trigger fan-out work, the storage layer should be able to recognize that the requested value is unchanged before producing more work.

The practical test is straightforward: restart the process several times without changing the logical session, then watch the durable queue. If the queue grows because recovery ran, recovery is not idempotent yet.

The most useful open-source fixes often look like this. The visible symptom is huge, the causal chain crosses runtime and storage boundaries, and the final patch is small because it restores one invariant: the same logical session keeps the same identity.

## Sources

- [Original contribution: claude-mem PR #3597](https://github.com/thedotmack/claude-mem/pull/3597)
- [Merged rehost: claude-mem PR #4032](https://github.com/thedotmack/claude-mem/pull/4032)
- [Release notes: claude-mem v13.24.18](https://github.com/thedotmack/claude-mem/releases/tag/v13.24.18)
