Skip to content

fix(channels): dedupe cloud channel context echo so prompt shows once#2806

Merged
raquelmsmith merged 3 commits into
mainfrom
posthog-code/fix-cloud-channel-context-duplicate
Jun 20, 2026
Merged

fix(channels): dedupe cloud channel context echo so prompt shows once#2806
raquelmsmith merged 3 commits into
mainfrom
posthog-code/fix-cloud-channel-context-duplicate

Conversation

@raquelmsmith

Copy link
Copy Markdown
Member

Problem

In cloud mode, a channel task's prompt appeared to be missing its CONTEXT.md — and then the prompt showed up again, this time with the context. Local tasks were fine.

The context was actually being delivered to the agent the whole time (the saga folds the channel CONTEXT.md into pendingUserMessage at task creation). The visible bug was a duplicated user message in the conversation:

  • The optimistic placeholder bubble is seeded from the bare task description (sessionService.hydrateCloudTaskSessionFromLogs), with no channel context.
  • The echoed session/prompt that streams back from the sandbox carries the appended <channel_context>…</channel_context> block.
  • mergeConversationItems deduped optimistic↔echo by exact content equality, so the context-bearing echo never matched its placeholder → both rendered.

Local sessions don't hit this — they swap optimistic→real by id, not by content.

Changes

  • mergeConversationItems now compares on the channel-context-stripped text (reusing extractChannelContext), so the echo dedupes against its placeholder.
  • When a match is found, the pinned bubble is upgraded to the richer echoed copy, so the CONTEXT.md chip renders in place — once — instead of as a second message.
  • Added a regression test covering the appended-context echo case.

Why: the channel context plumbing worked end-to-end, but cloud users saw the prompt twice (the second copy holding the context), which read as "context wasn't included." This makes the cloud conversation match local.

How did you test this?

  • pnpm --filter @posthog/ui test src/features/sessions/components — 86 passed (incl. new mergeConversationItems case + channelContext/UserMessage).
  • pnpm --filter @posthog/ui typecheck — clean.
  • biome check on the changed files — clean.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Created with PostHog Code

In cloud mode the channel's CONTEXT.md *is* delivered to the agent (folded
into pendingUserMessage at task creation), but the conversation rendered the
user's prompt twice: first the optimistic placeholder (seeded from the bare
task description, no context), then the echoed session/prompt that carries the
appended <channel_context> block.

mergeConversationItems deduped optimistic↔echo by exact content equality, so
the context-bearing echo never matched its placeholder and both rendered —
reading as "context shows up a second time / wasn't included the first time".
Local sessions don't hit this (they swap optimistic→real by id).

Compare on the channel-context-stripped text instead, and upgrade the pinned
bubble to the richer echoed copy so the CONTEXT.md chip renders in place,
without a duplicate.

Generated-By: PostHog Code
Task-Id: e2321274-b00c-4451-8411-a80a515a8965
@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit 7c80b9d.

@greptile-apps

greptile-apps Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
packages/ui/src/features/sessions/components/mergeConversationItems.test.ts:68-71
The expression `pinned?.type === "user_message" && pinned.content` is used as the actual value passed to `.toBe(...)`. When the assertion fails (e.g. `pinned` is `undefined` or the content doesn't match), Vitest reports the received value as `false` rather than the actual content string or `undefined`, making the failure message cryptic. Splitting into two separate assertions gives a precise message for each failure condition.

```suggestion
    const pinned = result.find((i) => i.id === "opt");
    expect(pinned?.type).toBe("user_message");
    expect((pinned as Extract<(typeof result)[number], { type: "user_message" }>).content).toBe(
      echoedWithContext,
    );
```

Reviews (1): Last reviewed commit: "fix(channels): dedupe cloud channel cont..." | Re-trigger Greptile

Comment on lines +68 to +71
const pinned = result.find((i) => i.id === "opt");
expect(pinned?.type === "user_message" && pinned.content).toBe(
echoedWithContext,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The expression pinned?.type === "user_message" && pinned.content is used as the actual value passed to .toBe(...). When the assertion fails (e.g. pinned is undefined or the content doesn't match), Vitest reports the received value as false rather than the actual content string or undefined, making the failure message cryptic. Splitting into two separate assertions gives a precise message for each failure condition.

Suggested change
const pinned = result.find((i) => i.id === "opt");
expect(pinned?.type === "user_message" && pinned.content).toBe(
echoedWithContext,
);
const pinned = result.find((i) => i.id === "opt");
expect(pinned?.type).toBe("user_message");
expect((pinned as Extract<(typeof result)[number], { type: "user_message" }>).content).toBe(
echoedWithContext,
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/sessions/components/mergeConversationItems.test.ts
Line: 68-71

Comment:
The expression `pinned?.type === "user_message" && pinned.content` is used as the actual value passed to `.toBe(...)`. When the assertion fails (e.g. `pinned` is `undefined` or the content doesn't match), Vitest reports the received value as `false` rather than the actual content string or `undefined`, making the failure message cryptic. Splitting into two separate assertions gives a precise message for each failure condition.

```suggestion
    const pinned = result.find((i) => i.id === "opt");
    expect(pinned?.type).toBe("user_message");
    expect((pinned as Extract<(typeof result)[number], { type: "user_message" }>).content).toBe(
      echoedWithContext,
    );
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

…ows immediately

The cloud sandbox takes seconds to boot and echo the prompt back, so the
optimistic placeholder was seeded from the bare task description and showed no
CONTEXT.md chip until the echo landed.

Task creation now hands the context-bearing pendingUserMessage to the session
service (rememberInitialCloudPrompt), which seeds the placeholder with it — so
the chip renders the instant the user submits, with the <channel_context> block
stripped to a chip by UserMessage as usual (never raw XML).

Best-effort and in-memory: lost on a reload during the boot window, where the
merge-layer dedupe still renders the echo correctly (chip, no duplicate).

Generated-By: PostHog Code
Task-Id: e2321274-b00c-4451-8411-a80a515a8965
@raquelmsmith raquelmsmith marked this pull request as ready for review June 20, 2026 20:59
Addresses Greptile review: the combined `pinned?.type === "user_message" &&
pinned.content` expression reported `false` on failure instead of the actual
content. Assert the type, narrow, then assert the content separately.

Generated-By: PostHog Code
Task-Id: e2321274-b00c-4451-8411-a80a515a8965
@greptile-apps

greptile-apps Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Reviews (2): Last reviewed commit: "test(channels): split combined assertion..." | Re-trigger Greptile

@raquelmsmith raquelmsmith merged commit 3c2de4a into main Jun 20, 2026
23 checks passed
@raquelmsmith raquelmsmith deleted the posthog-code/fix-cloud-channel-context-duplicate branch June 20, 2026 21:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant