-
Notifications
You must be signed in to change notification settings - Fork 707
Description
Summary
projected assistant messages are effectively keyed by message_id alone. If two different threads reuse the same provider-backed assistant message identity, the second thread overwrites the first.
That breaks thread isolation and can surface after reconnect/reload as the wrong assistant reply, a missing reply, or checkpoint/diff UI linked to the wrong message.
Why this matters
This is a reliability issue in a core flow:
- thread state stops being isolated
- reconnect/reload becomes unpredictable
- provider/runtime ID reuse can leak one thread's projected state into another
Deterministic repro on upstream/main
I reproduced this against fetched upstream/main at commit 065ef922 on March 11, 2026.
- Upsert a projected assistant message row for
thread-shared-message-awith:messageId = message-shared-across-threadstext = first thread text
- Upsert a second projected assistant message row for
thread-shared-message-bwith the samemessageIdand:text = second thread text
- Call
listByThreadIdfor both threads.
Expected
Both threads retain one projected row with their own text.
Actual
The first thread loses its row entirely, and only the second thread's row remains:
{
"firstRows": [],
"secondRows": [
{
"messageId": "message-shared-across-threads",
"threadId": "thread-shared-message-b",
"text": "second thread text"
}
]
}Where it happens
The persistence layer currently upserts projection_thread_messages on message_id alone in:
apps/server/src/persistence/Layers/ProjectionThreadMessages.ts
That means cross-thread reuse is treated as the same logical row.
Broader user-facing impact
This appears to be the same class of bug behind cross-thread assistant/checkpoint identity collisions when provider itemId or fallback turnId values are reused across threads. In the UI that can show up as:
- one thread showing another thread's assistant reply
- one thread losing its own assistant reply after reload/reconnect
- checkpoint/diff linkage attaching to the wrong assistant message
Fix direction
A focused fix seems to be:
- scope projected assistant identities by thread
- key
projection_thread_messagesby(thread_id, message_id)instead ofmessage_idalone - make any fallback assistant/checkpoint IDs thread-aware as well
I verified the same regression input passes in a local fix worktree once thread scoping is applied: both rows remain isolated.