Root and reply threads: dedupe and enforce a unique key - #435
Merged
TheGreatAxios merged 3 commits intoAug 29, 2026
Merged
TheGreatAxios merged 3 commits into
TheGreatAxios merged 3 commits into
Conversation
TheGreatAxios
force-pushed
the
cl-7130-root-and-reply-threads-are-select-then-insert-with-no-unique
branch
from
August 28, 2026 18:55
30aba3f to
ed3979f
Compare
TheGreatAxios
changed the base branch from
main
to
cl-6687-rotated-api-keys-never-reach-live-agents
August 28, 2026 18:55
TheGreatAxios
force-pushed
the
cl-7130-root-and-reply-threads-are-select-then-insert-with-no-unique
branch
from
August 29, 2026 04:59
ed3979f to
7fdf5bf
Compare
TheGreatAxios
force-pushed
the
cl-7130-root-and-reply-threads-are-select-then-insert-with-no-unique
branch
2 times, most recently
from
August 29, 2026 17:28
901b82e to
7735525
Compare
TheGreatAxios
force-pushed
the
cl-7130-root-and-reply-threads-are-select-then-insert-with-no-unique
branch
5 times, most recently
from
August 29, 2026 18:12
4242d0a to
5276c77
Compare
TheGreatAxios
commented
Aug 29, 2026
TheGreatAxios
left a comment
Contributor
Author
There was a problem hiding this comment.
critique · comment
Partial unique indexes plus insert-on-conflict-reselect keep one root/reply thread per key.
No uniqueness defect. This branch is stacked on #427; merge order is 427 then 435, or rebase onto main after 427.
- packages/chat/src/threads.ts — createDeliveryThread is still select-then-insert. Out of CL-7130 scope.
Walking-skeleton red on this PR is main deleting DATABASE_URL after memory-mount tests (CL-7182 / #465), not this uniqueness diff.
Base automatically changed from
cl-6687-rotated-api-keys-never-reach-live-agents
to
main
August 29, 2026 22:34
Extends migrations.test.ts to seed two duplicate root threads (with a message pointed at the newer one) and prove the coming migration collapses them onto the oldest row, repointing thread membership, and that the resulting partial unique indexes exist. Adds threads.drizzle.test.ts, a DB-gated suite (mirroring reactions.drizzle.test.ts) that races two concurrent ensureRootThread and openReplyThread calls against a real Postgres connection pool.
ensureRootThread and anchoredReplyThread were select-then-insert with no unique constraint backing the read, so concurrent first writers could each insert a thread for the same (workbench, root) or (workbench, parent message, reply) key, and the LIMIT 1 read with no ORDER BY picked one nondeterministically. Migration 0025 dedupes any existing duplicates (keeping the oldest row, repointing thread membership and message thread pointers off the dropped rows) and adds partial unique indexes on workbench_threads. Both stores now insert with onConflictDoNothing and re-select on conflict (the toggleReaction pattern in reactions.ts), with a deterministic ORDER BY created_at on the existing reads. Fixes CL-7130.
TheGreatAxios
force-pushed
the
cl-7130-root-and-reply-threads-are-select-then-insert-with-no-unique
branch
from
August 29, 2026 22:35
5276c77 to
2719021
Compare
TheGreatAxios
commented
Aug 29, 2026
TheGreatAxios
left a comment
Contributor
Author
There was a problem hiding this comment.
critique · approve
Partial unique indexes, conflict-safe inserts, and 0025 keep-oldest repoint hold. walking-skeleton ran the DB-gated proofs green on main after #427.
- packages/chat/src/migrations.ts:462-468 — root/reply unique keys
- packages/chat/src/threads.ts:431-438 / 537-547 — onConflictDoNothing then re-select
- File-for-later: createDeliveryThread is still select-then-insert (out of CL-7130)
GitHub will not accept approve on the author's own PR; this comment is the review.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes CL-7130 — https://linear.app/abklabs/issue/CL-7130
Problem
ensureRootThread(packages/chat/src/threads.ts:388-419) andanchoredReplyThread(:454-495) SELECT ... LIMIT 1 then INSERT, andworkbench_threads(packages/chat/src/schema.ts:195-225) only had anon-unique index on
(tenant_id, workbench_id). Two concurrent firstwriters could each insert a duplicate
kind='root'row for the sameworkbench, or a duplicate
kind='reply'row for the sameparentMessageId, and theLIMIT 1read with noORDER BYthenpicked one of the duplicates nondeterministically.
Change
0025_workbench_threads_unique_key(next number after0023; skipping 0024 since PR Relaunch live agents when their inference credential rotates #427 already claims it as
0024_workbench_launch_sources_digest) dedupes any existingduplicate root/reply threads, keeping the oldest row per key and
repointing
workbench_thread_messages.thread_id,workbench_messages.thread_id, and anyworkbench_threads.parent_thread_idthat referenced a dropped duplicate, then adds partial unique indexes:
UNIQUE (tenant_id, workbench_id) WHERE kind = 'root'andUNIQUE (tenant_id, workbench_id, parent_message_id) WHERE kind = 'reply'.schema.tsviauniqueIndex(...).where(sql\...`)`.ensureRootThread/anchoredReplyThreadnow insert withonConflictDoNothing(targeting the new partial indexes) andre-select on conflict — the same pattern
reactions.ts'toggleReactionalready uses — instead of select-then-insert. Theexisting
LIMIT 1reads got a deterministicORDER BY created_at.Tests
packages/chat/test/migrations.test.ts: asserts the new partialindexes exist after a full migration run, and a new test seeds two
duplicate root threads (with a message pointed at the newer one)
before 0025 runs, then proves 0025 collapses them onto the oldest
row and repoints thread membership.
packages/chat/test/threads.drizzle.test.ts(new): DB-gated, racestwo concurrent
ensureRootThreadcalls and two concurrentopenReplyThreadcalls against a real Postgres pool, provingneither throws and both converge on one row.
bun run checklocally (it OOM-killed) — typecheck(
bunx tsc --noEmit -p packages/chat), the focused test files above(unit + DB-gated with
E2E_REQUIRED=1), prettier, and eslint on thechanged files all passed locally; CI is the gate for the rest.