fix(copilot): stop async chat auto-titling from clobbering an explicit rename - #7414
Conversation
…t rename Chat title generation is fired at turn start and resolves asynchronously, but its write was unconditional (`WHERE id = chatId`), re-checking nothing. The guard that decides whether to generate reads a snapshot taken at request start, so a title generated from that snapshot could land long after the user renamed the chat and overwrite the rename — a classic check-then-write race. A user who renames a chat shortly after their first message could silently lose the rename. Make the write conditional on the title still being unset, so the generated title fills an empty slot and can never overwrite an explicit one, and skip the follow-up notification when the write lost the race (so we never announce a title the row no longer holds). Both auto-title call sites are fixed: the interactive turn path (lib/copilot/request/lifecycle/start.ts) and the inbox task path (lib/mothership/inbox/executor.ts), which had the identical unconditional write.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
@cubic-dev-ai review |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
Greptile SummaryThis PR prevents asynchronous auto-title generation from overwriting an explicit chat rename by making both title writes conditional on the title remaining null. It also suppresses title notifications when the conditional update loses the race.
Confidence Score: 5/5The PR appears safe to merge, with both asynchronous title paths correctly preserving explicit renames. The conditional PostgreSQL updates atomically write only to null titles, and the returned-row checks prevent publishing a generated title when a concurrent rename has already won.
|
| Filename | Overview |
|---|---|
| apps/sim/lib/copilot/request/lifecycle/start.ts | Makes interactive auto-title persistence conditional and publishes the generated title only when the database update succeeds. |
| apps/sim/lib/mothership/inbox/executor.ts | Applies the same atomic title-write and notification behavior to inbox task execution. |
Sequence Diagram
sequenceDiagram
participant Generator as Async title generator
participant DB as Chat database
participant User as User rename
participant Events as Title events
Generator->>Generator: Begin generating title
User->>DB: Set explicit title
Generator->>DB: Update title where title IS NULL
DB-->>Generator: No affected rows
Generator-->>Events: Skip generated-title notification
Reviews (1): Last reviewed commit: "fix(copilot): stop async chat auto-titli..." | Re-trigger Greptile
Summary
Chat title generation is fired at turn start and resolves asynchronously, but its write was unconditional:
The guard that decides whether to generate a title (
if (!chatId || currentChat?.title || !isNewChat) return) evaluates a snapshot taken at request start. The write then happens much later with no re-check — a classic check-then-write (TOCTOU) race. If a user renames the chat while the title is being generated, the generated title lands afterward and silently overwrites the rename.This is reachable in normal use: rename a chat shortly after sending the first message and the rename can disappear.
Fix
Make the write conditional on the title still being unset, so a generated title can only ever fill an empty slot:
copilotChats.titleis nullable with no default, soisNullis exactly the "no title yet" state. The update is atomic, so the explicit rename deterministically wins. When the write loses the race (0 rows), the follow-up title notification is skipped as well — we should not announce a title the row no longer holds.Both auto-title call sites are fixed, since they had the identical unconditional write:
lib/copilot/request/lifecycle/start.ts— the interactive turn pathlib/mothership/inbox/executor.ts— the inbox task pathWhy not "eliminate the race" instead
The concurrency is inherent — an async background title generator runs alongside user actions, and awaiting it would block the turn on a title LLM call. The race cannot be removed, only made well-defined; a conditional (compare-and-set) update is the standard way to do that, and it makes the outcome deterministic: an explicit title always wins, a generated one only fills a gap.
Testing
tsc --noEmitandbiomeclean. Existing unit tests for both changed paths pass (start.test.ts,executor.test.ts— 13 tests). This also removes the root cause of a flaky integration check that asserted a forked chat inherits its parent's renamed title.Type of change