Skip to content

Delivery threads: dedupe and enforce a unique key - #505

Merged
TheGreatAxios merged 2 commits into
mainfrom
cl-7199-delivery-thread-race
Aug 30, 2026
Merged

TheGreatAxios merged 2 commits into
mainfrom
cl-7199-delivery-thread-race

Conversation

@TheGreatAxios

Copy link
Copy Markdown
Contributor

Summary

createDeliveryThread was select-then-insert on (kind='delivery', runRef) with no unique constraint backing the read — unlike its ensureRootThread/anchoredReplyThread siblings in the same file, fixed for the same bug class in CL-7130. A redelivered routine event could insert a second delivery thread for the same run, splitting replies across both.

  • Migration 0026_workbench_threads_delivery_key dedupes any existing duplicate delivery threads (keeping the oldest row per tenant_id, workbench_id, run_ref), repointing workbench_thread_messages.thread_id, workbench_messages.thread_id, and workbench_threads.parent_thread_id off dropped rows before deleting them, then adds a partial unique index scoped to kind = 'delivery' AND run_ref IS NOT NULL.
  • createDeliveryThread now inserts with onConflictDoNothing (arbiter predicate matching the index exactly) and re-selects on conflict — the identical structural pattern already used for root/reply threads.

Fixes CL-7199.

Test plan

  • cd packages/chat && bun run typecheck — clean
  • bun test (full package suite, real Postgres) — 745 pass, 0 fail
  • New migration test seeds pre-existing duplicate delivery threads plus null-run-ref rows, a message and a reply thread anchored on a dropped duplicate — asserts dedupe, repoint, null-exclusion, and post-migration index enforcement
  • New concurrency test: two racing createDeliveryThread calls for the same run converge on one row
  • Verified both new tests fail without the implementation commit (red), pass with it (green)
  • bunx prettier --check and bun run lint on touched files — clean

createDeliveryThread is select-then-insert on (kind='delivery',
runRef) with no unique index backing the read, so a redelivered
routine event can create two delivery threads for one run. Assert the
fix CL-7130 already established for root/reply threads: a concurrency
test proving two racing createDeliveryThread calls converge on one
row, and a migration test proving existing duplicate delivery threads
get deduped (with references repointed) before the unique index is
added.

These currently fail: no unique index or insert-first path exists yet
for delivery threads.
createDeliveryThread was select-then-insert on (kind='delivery',
runRef) with no unique constraint backing the read, unlike its
ensureRootThread/anchoredReplyThread siblings fixed for the same class
of bug in CL-7130. A redelivered routine event could insert a second
delivery thread for the same run, splitting replies across both.

Migration 0026 dedupes any existing duplicate delivery threads
(keeping the oldest row, repointing thread membership and message
thread pointers off the dropped rows) and adds a partial unique index
on workbench_threads scoped to kind='delivery' with a non-null
run_ref. createDeliveryThread now inserts with onConflictDoNothing and
re-selects on conflict, matching its siblings' pattern exactly.

Fixes CL-7199.

@TheGreatAxios TheGreatAxios left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critique · reviewed, no blocking findings

Reviewed the two commits (tests, then implementation) against CL-7130's precedent for this exact bug class.

  • packages/chat/src/migrations.ts:0026 — repoint-before-delete ordering correct (thread_id refs, then parent_thread_id, then delete duplicates, then index); byte-for-byte structural clone of the proven 0025 migration; null run_ref correctly excluded from dedupe to match the partial index predicate; runs inside the existing per-migration transaction so it's atomic; idempotent via IF NOT EXISTS and the migration ledger.
  • packages/chat/src/threads.ts:594-604 — onConflictDoNothing arbiter (target columns + where predicate) matches workbench_threads_delivery_key in schema.ts exactly, same pattern as the root/reply paths.
  • Verified independently: reverted the implementation commit and ran the new tests against real Postgres — 3 failures (missing migration, dedupe test, concurrency test diverging on ids). Restored HEAD — 7 pass, 0 fail. tsc clean.
  • No signature change to createDeliveryThread; routes.ts's single caller unaffected.

No issues found in scope.

@TheGreatAxios
TheGreatAxios merged commit ea751b4 into main Aug 30, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant