Skip to content

fix(conversations): Keep the root actor when later turns record execution - #2436

Merged
dcramer merged 1 commit into
mainfrom
fix/conversation-root-actor-event-turns
Oct 30, 2026
Merged

dcramer merged 1 commit into
mainfrom
fix/conversation-root-actor-event-turns

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Refs #2431 (bug #2: a web conversation's root actor gets replaced by Local CLI).

Root cause

SqlStore.recordExecution built the conversation from the arguments of the current write only. recordActivity already started from the saved row and used mergeActor, but recordExecution did not. Two callers send it per-Turn values:

  • turn-cursor.ts sends source: "local" for any non-web Turn on a web conversation, because web conversations use a local destination. sessionLogActor drops non-Slack actors, so it also sends no actor.
  • task-execution/store.ts (recordExecutionMetadata) sends the actor and source from the Redis work state. If that state has expired (7 days) and a watch event or a child result arrives, the work state is created again with source event and no actor. Slack conversations hit this path too.

With no actor, actorIdentityForConversation invented a system identity from that per-Turn source (Local CLI or Event). The upsert then wrote it with coalesce(excluded.actor_identity_id, existing), so it replaced the real root actor. The source column itself is set-once, so only the actor changed.

What was wrong with the local-cli fallback

The fallback is not wrong in itself. Real CLI conversations never store a user actor, so a Local CLI system identity for a conversation that started as local is correct. The bug is that the fallback ran against the source of the current Turn, not the conversation's saved origin. A web conversation is never local in SQL, but an event Turn on it looks local at the write site.

Fix

recordExecution now takes the actor and source from the saved conversation first, the same way recordActivity does: mergeActor(existing.actor, args.actor) and existing.source ?? args.source. All three callers of upsertConversation (createChild, recordActivity, recordExecution) now build from the saved row, so this one change covers both execution callers and both Slack and web roots.

Why not make actor_identity_id set-once in the upsert: today a conversation that a system started (for example by the scheduler) gets its root actor upgraded when the first person replies, through recordActivity and mergeActor. A set-once column would change that. That is a separate product decision. This fix keeps existing behavior and stops the incorrect overwrite.

Not changed

  • sessionLogActor still drops non-Slack actors. With this fix, a missing actor no longer causes an overwrite. Passing web actors through can be done later if a write needs them.
  • readConversationParticipants (api/conversations/participants.ts) still includes system identities. After this fix, a system identity is the root actor only for conversations that a system started (scheduler, event, real CLI). Hiding those from the avatar stack is a display decision, and the dashboard already falls back to the root actor when the list is empty.

Existing data

This does not repair rows that are already wrong. A possible backfill, not run anywhere: for root conversations with source = 'web' whose actor_identity_id points at the junior/local-cli system identity, set actor_identity_id to the identity of the earliest user message event in junior_conversation_events for that conversation. Slack roots overwritten with the junior/event identity can be repaired the same way when source = 'slack'. Both queries are idempotent and change only rows whose actor is a system identity that does not match their source.

via David Cramer.

--

View Session [Sentry]

…tion

Co-Authored-By: David Cramer <david@sentry.io>
@vercel

vercel Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
junior-docs Ready Ready Preview Oct 30, 2026 3:53pm UTC

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

3 screenshot changes — 3 changed · 0 added · 0 removed

Review screenshots in Frameshift

Conversation Child Agent Drawer · Desktop
Conversation Child Agent Drawer · Desktop
Changed
Conversation Child Agent Drawer · Mobile
Conversation Child Agent Drawer · Mobile
Changed
Conversation Share Details · Desktop
Conversation Share Details · Desktop
Changed

@dcramer
dcramer merged commit 295901f into main Oct 30, 2026
45 checks passed
@dcramer
dcramer deleted the fix/conversation-root-actor-event-turns branch October 30, 2026 16:15
sentry-junior Bot added a commit that referenced this pull request Oct 30, 2026
Before #2436 the coalesce write could also replace one human Identity with
a later human Identity. A multi-author root with a user Identity today is
then no evidence against the first-author rule. Run the agreement check and
the repair only on roots with one author User, and count the rest.
sentry-junior Bot added a commit that referenced this pull request Oct 30, 2026
Event Messages and bot Messages have no author Identity today. An event Turn
is how a root got the event system Identity before #2436, so requiring an
author on every user Message would skip the damaged rows. Count authors
only on human user Messages, as isEventConversationMessage and the isBot
flag define them.

This branch was successfully deployed

1 active deployment
Preview – junior-docs — 05c57f51 Deployed Oct 30, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: high PR risk score: high

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant