Context
handleMessage in lib/chat.js does a read-modify-write on each user's conversation history (convoStore.get → chat.chat → convoStore.set). Two messages arriving close together from the same user would otherwise race: both read the same base history and the second set() clobbers the first turn ("Data forgot what I just said").
This was fixed in the commit that added withUserLock by serializing each user's turns through an in-process promise chain.
The limitation
The lock lives in a module-level Map in a single Node process. It is correct only while the bot runs as exactly one process — which is true today (single bot-data.service systemd unit).
The moment the bot runs as more than one replica, or a second process talks to the same Redis, the in-memory lock no longer serializes anything and the read-modify-write race returns across processes.
What to do if/when we go multi-replica
Replace the in-process Map lock with a Redis-level primitive on the per-user key, e.g.:
- Optimistic concurrency with
WATCH/MULTI/EXEC (retry on conflict), or
- A short-lived advisory lock via
SET <lockkey> <token> NX PX <ttl> with token-checked release.
Deliberately not done now: it adds a Redis round-trip to every turn and is over-engineering for a single-process bot. This issue exists so the single-process assumption is documented rather than a silent trap.
Pointers
lib/chat.js — withUserLock + handleMessage
- Concurrency regression test:
test/chat.test.js — "serializes concurrent turns from the same user"
Context
handleMessageinlib/chat.jsdoes a read-modify-write on each user's conversation history (convoStore.get→chat.chat→convoStore.set). Two messages arriving close together from the same user would otherwise race: both read the same base history and the secondset()clobbers the first turn ("Data forgot what I just said").This was fixed in the commit that added
withUserLockby serializing each user's turns through an in-process promise chain.The limitation
The lock lives in a module-level
Mapin a single Node process. It is correct only while the bot runs as exactly one process — which is true today (singlebot-data.servicesystemd unit).The moment the bot runs as more than one replica, or a second process talks to the same Redis, the in-memory lock no longer serializes anything and the read-modify-write race returns across processes.
What to do if/when we go multi-replica
Replace the in-process
Maplock with a Redis-level primitive on the per-user key, e.g.:WATCH/MULTI/EXEC(retry on conflict), orSET <lockkey> <token> NX PX <ttl>with token-checked release.Deliberately not done now: it adds a Redis round-trip to every turn and is over-engineering for a single-process bot. This issue exists so the single-process assumption is documented rather than a silent trap.
Pointers
lib/chat.js—withUserLock+handleMessagetest/chat.test.js— "serializes concurrent turns from the same user"