From 4d49ba4d6d5e8d321b1edd0d3b1b104348e35071 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:18:49 +0000 Subject: [PATCH 1/2] fix(chat): strip runtime turn context from projection before model input When a follow-up message arrives in an existing Slack thread, the session log projection may contain runtime turn context blocks that were written by prior turns (e.g. from the original thread starter). These blocks carry the earlier requester's identity. `loadPiMessagesForTurn` checked `hasRuntimeTurnContext` against the raw projection. If it returned true, the current turn's context (including the actual requester for this message) was never injected into the prompt. The model then used the stale requester identity for attributions such as the `Action taken on behalf of` line in PR bodies. The active-turn resume path already calls `stripRuntimeTurnContext` before returning piMessages. Apply the same strip to the projection path so both paths are consistent and fresh requester context is always injected for the current turn. Adds a regression test covering the multi-user thread scenario where the projection carries a stale requester block from user A, and user B's follow-up must inject fresh requester context. Refs: https://sentry.slack.com/archives/C0AHB7N2JCR/p1780911303272149 Co-authored-by: no --- .../junior/src/chat/runtime/reply-executor.ts | 2 +- .../respond-helpers-runtime-context.test.ts | 62 ++++++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/packages/junior/src/chat/runtime/reply-executor.ts b/packages/junior/src/chat/runtime/reply-executor.ts index aaa30563e..b1d6156f0 100644 --- a/packages/junior/src/chat/runtime/reply-executor.ts +++ b/packages/junior/src/chat/runtime/reply-executor.ts @@ -214,7 +214,7 @@ async function loadPiMessagesForTurn(args: { if (projection.length > 0) { return { canCompact: true, - piMessages: projection, + piMessages: stripRuntimeTurnContext(projection), }; } diff --git a/packages/junior/tests/unit/misc/respond-helpers-runtime-context.test.ts b/packages/junior/tests/unit/misc/respond-helpers-runtime-context.test.ts index b4b568eb7..7037b0bb7 100644 --- a/packages/junior/tests/unit/misc/respond-helpers-runtime-context.test.ts +++ b/packages/junior/tests/unit/misc/respond-helpers-runtime-context.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from "vitest"; import type { PiMessage } from "@/chat/pi/messages"; -import { prependMissingRuntimeTurnContext } from "@/chat/respond-helpers"; +import { + hasRuntimeTurnContext, + prependMissingRuntimeTurnContext, + stripRuntimeTurnContext, +} from "@/chat/respond-helpers"; describe("prependMissingRuntimeTurnContext", () => { it("leaves recorded bootstrap prompts unchanged", () => { @@ -47,6 +51,62 @@ describe("prependMissingRuntimeTurnContext", () => { expect(JSON.stringify(updated[0])).not.toContain("slack:C123:updated"); }); + it("injects new requester context after stripping stale projected context", () => { + // Simulates a thread started by user A (max.topolsky), where the runtime + // turn context from the first turn was persisted into the session log + // projection. When user B (nelson.osacky) sends a follow-up message, + // `loadPiMessagesForTurn` must strip the stale context before returning, + // so `hasRuntimeTurnContext` returns false and a fresh context block + // carrying the new requester is injected. + const projectionWithStaleContext: PiMessage[] = [ + { + role: "user", + content: [ + { + type: "text", + text: [ + "", + "", + "- full_name: Max Topolsky", + "- user_name: max.topolsky", + "- user_id: U_MAX", + "", + "", + ].join("\n"), + }, + { type: "text", text: "what command is the plugin using?" }, + ], + timestamp: 1, + }, + { + role: "assistant", + content: [{ type: "text", text: "it uses sentry-cli build snapshots" }], + timestamp: 2, + }, + ] as PiMessage[]; + + // Before stripping: stale context is present, so no new context would be injected + expect(hasRuntimeTurnContext(projectionWithStaleContext)).toBe(true); + + // After stripping (what loadPiMessagesForTurn now does): fresh injection is possible + const stripped = stripRuntimeTurnContext(projectionWithStaleContext); + expect(hasRuntimeTurnContext(stripped)).toBe(false); + + const nelsonContext = [ + "", + "", + "- user_name: nelson.osacky", + "- user_id: U_NELSON", + "", + "", + ].join("\n"); + const updated = prependMissingRuntimeTurnContext(stripped, nelsonContext); + + expect(JSON.stringify(updated)).toContain("nelson.osacky"); + expect(JSON.stringify(updated)).not.toContain("max.topolsky"); + expect(JSON.stringify(updated)).not.toContain("Max Topolsky"); + }); + it("adds bootstrap context to a pre-prompt user boundary", () => { const messages: PiMessage[] = [ { From 89056079fce9e946929b478eb827ac186d69ea25 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:41:55 +0000 Subject: [PATCH 2/2] test(chat): replace real user names with placeholders in regression test Co-authored-by: no --- .../respond-helpers-runtime-context.test.ts | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/junior/tests/unit/misc/respond-helpers-runtime-context.test.ts b/packages/junior/tests/unit/misc/respond-helpers-runtime-context.test.ts index 7037b0bb7..68ce4a076 100644 --- a/packages/junior/tests/unit/misc/respond-helpers-runtime-context.test.ts +++ b/packages/junior/tests/unit/misc/respond-helpers-runtime-context.test.ts @@ -52,12 +52,12 @@ describe("prependMissingRuntimeTurnContext", () => { }); it("injects new requester context after stripping stale projected context", () => { - // Simulates a thread started by user A (max.topolsky), where the runtime - // turn context from the first turn was persisted into the session log - // projection. When user B (nelson.osacky) sends a follow-up message, - // `loadPiMessagesForTurn` must strip the stale context before returning, - // so `hasRuntimeTurnContext` returns false and a fresh context block - // carrying the new requester is injected. + // Simulates a thread started by user A, where the runtime turn context + // from the first turn was persisted into the session log projection. + // When user B sends a follow-up message, `loadPiMessagesForTurn` must + // strip the stale context before returning, so `hasRuntimeTurnContext` + // returns false and a fresh context block carrying user B's identity + // is injected instead. const projectionWithStaleContext: PiMessage[] = [ { role: "user", @@ -67,20 +67,20 @@ describe("prependMissingRuntimeTurnContext", () => { text: [ "", "", - "- full_name: Max Topolsky", - "- user_name: max.topolsky", - "- user_id: U_MAX", + "- full_name: User Alpha", + "- user_name: user.alpha", + "- user_id: U_ALPHA", "", "", ].join("\n"), }, - { type: "text", text: "what command is the plugin using?" }, + { type: "text", text: "original question" }, ], timestamp: 1, }, { role: "assistant", - content: [{ type: "text", text: "it uses sentry-cli build snapshots" }], + content: [{ type: "text", text: "original answer" }], timestamp: 2, }, ] as PiMessage[]; @@ -92,19 +92,19 @@ describe("prependMissingRuntimeTurnContext", () => { const stripped = stripRuntimeTurnContext(projectionWithStaleContext); expect(hasRuntimeTurnContext(stripped)).toBe(false); - const nelsonContext = [ + const userBContext = [ "", "", - "- user_name: nelson.osacky", - "- user_id: U_NELSON", + "- user_name: user.beta", + "- user_id: U_BETA", "", "", ].join("\n"); - const updated = prependMissingRuntimeTurnContext(stripped, nelsonContext); + const updated = prependMissingRuntimeTurnContext(stripped, userBContext); - expect(JSON.stringify(updated)).toContain("nelson.osacky"); - expect(JSON.stringify(updated)).not.toContain("max.topolsky"); - expect(JSON.stringify(updated)).not.toContain("Max Topolsky"); + expect(JSON.stringify(updated)).toContain("user.beta"); + expect(JSON.stringify(updated)).not.toContain("user.alpha"); + expect(JSON.stringify(updated)).not.toContain("User Alpha"); }); it("adds bootstrap context to a pre-prompt user boundary", () => {