Never hide a real agent reply behind narration heuristics

Turns visibly worked but never answered: two filters could swallow a
genuinely sent reply. The chat timeline ran persisted agent messages
through the transcript's internal-narration patterns, so replies like
"Done! I've sent the summary…" were dropped outright; and the
consecutive-run collapse preferred the last "human-facing" message,
hiding a narration-styled FINAL reply behind an earlier one.

Persisted messages now render on content alone (narration shaping
stays in the activity transcript, dedup still applies), and the run
collapse hides only interim narration — every substantive message
stays, and the run's final message always stays regardless of
phrasing. Regression spec: a narration-styled final reply must render
alongside the earlier PR announcement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
klopez4212
2026-07-07 07:52:31 +01:00
co-authored by Claude Fable 5
parent 1f923a89ef
commit 3dcf6b0a62
3 changed files with 33 additions and 15 deletions
+10 -7
View File
@@ -159,14 +159,17 @@ function addIntermediateAgentTurnMessageIds({
return;
}
const keepMessage =
[...agentRun]
.reverse()
.find((message) => isHumanFacingAssistantText(message.content)) ??
agentRun[agentRun.length - 1];
// Hide only interim narration: substantive messages (PR announcements,
// answers) all stay, and the run's FINAL message always stays even when
// narration-styled — a real reply must never lose to a phrasing
// heuristic, which is exactly how turns ended up visibly working but
// never answering.
const lastId = agentRun[agentRun.length - 1].id;
for (const message of agentRun) {
if (message.id !== keepMessage.id) {
if (
message.id !== lastId &&
!isHumanFacingAssistantText(message.content)
) {
hiddenIds.add(message.id);
}
}
+8 -8
View File
@@ -302,22 +302,22 @@ export function ChatDetail({
) {
return false;
}
const isAgent =
defaultAgent?.pubkey != null &&
normalizePubkey(message.pubkey) ===
normalizePubkey(defaultAgent.pubkey);
// NOTE: no narration heuristics here. A persisted agent message was
// deliberately SENT to the channel — filtering it through the
// transcript's internal-narration patterns dropped real replies
// ("Done! I've sent the summary…"), leaving turns that visibly
// worked but never answered. Narration shaping belongs to the
// activity transcript; persisted rows only dedup against it.
return (
(eventHasTag(message, "chat_context", "source") ||
(isAgent
? isHumanFacingAssistantText(message.content)
: message.content.trim().length > 0)) &&
message.content.trim().length > 0) &&
!shouldHidePersistedAgentMessage({
event: message,
hiddenAgentMessageIds: chatActivity.hiddenAgentMessageIds,
})
);
}),
[chatActivity.hiddenAgentMessageIds, defaultAgent?.pubkey, messages],
[chatActivity.hiddenAgentMessageIds, messages],
);
const hasTranscriptActivity = chatActivity.totalBlockCount > 0;
@@ -118,6 +118,15 @@ test("first message in a new chat is sent and rendered", async ({ page }) => {
createdAt: base + 2,
pubkey: pubkey ?? undefined,
});
// A narration-styled FINAL reply must still render: persisted
// messages are never filtered by the transcript's narration
// heuristics (this exact phrasing used to be silently dropped).
win.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "Hello Fizz, first message",
content: "Done! I've sent the summary with details to the channel.",
createdAt: base + 3,
pubkey: pubkey ?? undefined,
});
// A human message with a mention tag: @bob must render as a chip
// (alice's pubkey is a mock profile fixture; bob's resolves via the
// message's p tag).
@@ -135,6 +144,12 @@ test("first message in a new chat is sent and rendered", async ({ page }) => {
{ pubkey: fizzPubkey },
);
await expect(
page
.getByLabel("Chat messages")
.getByText("Done! I've sent the summary with details to the channel."),
).toBeVisible({ timeout: 10_000 });
// Mentions in chat messages render as chips, same as channels.
await expect(
page.getByLabel("Chat messages").locator("[data-mention]", {