From 05c57f51b4756bec253c7ebefa55e0af2ee44105 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:52:33 +0000 Subject: [PATCH] fix(conversations): Keep the root actor when later turns record execution Co-Authored-By: David Cramer --- .../junior/src/chat/conversations/sql/store.ts | 9 +++++++-- .../tests/component/conversation-sql-store.test.ts | 14 +++++++++++++- .../integration/conversation-turn-work.test.ts | 3 +++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/junior/src/chat/conversations/sql/store.ts b/packages/junior/src/chat/conversations/sql/store.ts index b314b312cd..367399a9d7 100644 --- a/packages/junior/src/chat/conversations/sql/store.ts +++ b/packages/junior/src/chat/conversations/sql/store.ts @@ -510,6 +510,11 @@ export class SqlStore implements ConversationStore { const execution = incomingIsFresh ? args.execution : (existing?.execution ?? args.execution); + // Root Actor and origin Source come from the saved Conversation, as in + // recordActivity. A later Turn, such as an event on a web Conversation, + // must not derive a system Actor from its own Source. + const actor = mergeActor(existing?.actor, args.actor); + const source = existing?.source ?? args.source; await this.upsertConversation({ conversation: { schemaVersion: 1, @@ -523,8 +528,8 @@ export class SqlStore implements ConversationStore { ...(existing?.location ? { location: existing.location } : undefined), ...(args.channelName ? { channelName: args.channelName } : undefined), ...(args.destination ? { destination: args.destination } : undefined), - ...(args.actor ? { actor: args.actor } : undefined), - ...(args.source ? { source: args.source } : undefined), + ...(actor ? { actor } : undefined), + ...(source ? { source } : undefined), ...(args.title ? { title: args.title } : undefined), ...(args.visibility ? { visibility: args.visibility } : undefined), execution, diff --git a/packages/junior/tests/component/conversation-sql-store.test.ts b/packages/junior/tests/component/conversation-sql-store.test.ts index 4381659f7e..101286428e 100644 --- a/packages/junior/tests/component/conversation-sql-store.test.ts +++ b/packages/junior/tests/component/conversation-sql-store.test.ts @@ -646,7 +646,7 @@ describe("conversation SQL store", () => { } }, 15_000); - it("fills missing actor identity from later trusted profile observations", async () => { + it("fills missing actor identity from later observations and keeps it across event writes", async () => { const fixture = await createJuniorSqlFixture(); try { @@ -677,6 +677,17 @@ describe("conversation SQL store", () => { source: "slack", nowMs: 2_000, }); + // An event write without an Actor, such as a mailbox mirror after the + // Redis work state expired, must not replace the Slack root Actor. + await store.recordExecution({ + conversationId: CONVERSATION_ID, + createdAtMs: 1_000, + execution: { status: "idle", updatedAtMs: 3_000 }, + lastActivityAtMs: 3_000, + metrics: null, + source: "event", + updatedAtMs: 3_000, + }); await expect(store.listByActivity({ limit: 5 })).resolves.toMatchObject([ { @@ -686,6 +697,7 @@ describe("conversation SQL store", () => { fullName: "Casey Example", slackUserId: "U123", }, + source: "slack", }, ]); } finally { diff --git a/packages/junior/tests/integration/conversation-turn-work.test.ts b/packages/junior/tests/integration/conversation-turn-work.test.ts index cd451a00fb..405500b5c9 100644 --- a/packages/junior/tests/integration/conversation-turn-work.test.ts +++ b/packages/junior/tests/integration/conversation-turn-work.test.ts @@ -638,6 +638,9 @@ describe("Conversation mailbox Turn work", () => { source, state: "completed", }); + const root = await conversationStore.get({ conversationId }); + // The event Turn keeps the web root Actor. + expect(root?.actor).toMatchObject({ email: actor.email }); const transcript = ( await getConversationEventStore().loadHistory(conversationId) ).flatMap((event) =>