Skip to content

fix(desktop): strip controller-injected prefixes from chat display (#3720)#3738

Closed
ashishexee wants to merge 1 commit into
esengine:main-v2from
ashishexee:fix/3720-agent-prompt-leak
Closed

fix(desktop): strip controller-injected prefixes from chat display (#3720)#3738
ashishexee wants to merge 1 commit into
esengine:main-v2from
ashishexee:fix/3720-agent-prompt-leak

Conversation

@ashishexee

Copy link
Copy Markdown
Contributor

Closes #3720

Problem

Agent mode's injected system prompt ([Plan mode — read-only...]) and other controller-injected prefixes leak into the desktop chat UI as visible user messages. This affects:

  1. Old sessions (pre-v1.4.0) — no .display.json sidecar exists, so sessionDisplayResolver() falls back to returning raw composed content with PlanModeMarker, <memory-update>, and <background-jobs> blocks intact.
  2. Synthetic user messagesplanApprovedMessage, stream-recovery messages, readiness-retry messages, and executor-handoff messages are stored as Role: "user" but should never appear in the chat UI. The frontend only filters role === "system" — synthetic messages pass through.

The CLI does NOT have this leak for PlanModeMarker (it already strips it in replaySectionsFor), but it DOES leak <memory-update> and <background-jobs> blocks.

Solution

StripComposePrefixes (new function in internal/control/input.go)

Strips all controller-injected prefixes from a composed user message:

  • <memory-update>…</memory-update> blocks (prepended by Compose() when memory is queued)
  • <background-jobs>…</background-jobs> blocks (prepended by Compose() for job completions)
  • PlanModeMarker + "\n\n" prefix (prepended when plan mode is active)

Used as the fallback in sessionDisplayResolver() when no .display.json sidecar exists — so old sessions display correctly without backfill.

IsSyntheticUserMessage (new function in internal/control/input.go)

Detects known synthetic user messages by exact match (planApprovedMessage) or specific prefixes:

  • "Plan approved — plan mode is off"
  • "Host final-answer readiness check failed"
  • "You are already in the executor phase"
  • "The previous assistant response was interrupted while a tool call" / …during streaming" / …before visible"
  • "The previous assistant response finished without any visible answer"

Prefixes are specific enough to avoid false positives — a user typing "my response was interrupted by VPN" will NOT be filtered. A sync comment on the syntheticPrefixes var notes it must be kept in sync with the constants in controller.go and agent.go.

Integration points

  1. desktop/sessions.go:sessionDisplayResolver() — fallback changed from return content to return control.StripComposePrefixes(content) — fixes old-session leak
  2. desktop/app.go:historyMessages() — added if control.IsSyntheticUserMessage(content) { continue } — filters synthetic messages from history
  3. internal/cli/chat_tui.go:replaySectionsFor() — changed strings.TrimPrefix(m.Content, PlanModeMarker+"\n\n") to control.StripComposePrefixes(m.Content) — also strips memory/job blocks in CLI session replay

Why this approach

Alternative Why not
Frontend-only stripping Only handles PlanModeMarker, not memory/job blocks or synthetic messages. Fragile — depends on frontend staying in sync with Go compose logic.
Add Synthetic field to provider.Message Changes session format — breaks backward compatibility with old session files. Our IsSyntheticUserMessage is still needed as a fallback for those.
Record display for all synthetic messages Doesn't help old sessions that already exist without recordings.

This fix handles both old sessions (via StripComposePrefixes fallback) AND new sessions (via IsSyntheticUserMessage filter), with no session format changes.

Testing

18 new test cases:

  • TestStripComposePrefixes — 8 cases: plain message, plan marker stripped, marker alone, memory block, jobs block, memory+marker, empty after strip, memory-only
  • TestIsSyntheticUserMessage — 10 cases: plan approved, 3 stream recovery variants, empty final, readiness retry, executor handoff, regular message (false), plan marker (false), interrupted-before-visible variant, and a false-positive test confirming a user typing "my response was interrupted by VPN" is NOT filtered
go vet ./internal/control/... ./internal/cli/...  — clean
go test ./internal/control/... ./internal/cli/...  — pass
gofmt — clean (CRLF working-copy warnings only, committed content uses LF)

@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development desktop Wails desktop app (desktop/**) tui Terminal UI / CLI (internal/cli, internal/control) agent Core agent loop (internal/agent, internal/control) labels Jun 9, 2026
esengine added a commit that referenced this pull request Jun 10, 2026
…3720)

Strip controller-injected prefixes ([Plan mode] marker, <memory-update>, <background-jobs>) and synthetic Role:user messages from the chat display so agent-mode internals no longer leak into the desktop/TUI transcript. Closes #3738, Closes #3720.
@ashishexee ashishexee deleted the fix/3720-agent-prompt-leak branch June 10, 2026 03:48
@ashishexee ashishexee restored the fix/3720-agent-prompt-leak branch June 10, 2026 03:48
SuMuxi66 pushed a commit to SuMuxi66/DeepSeek-Reasonix that referenced this pull request Jun 10, 2026
…sengine#3720)

Strip controller-injected prefixes ([Plan mode] marker, <memory-update>, <background-jobs>) and synthetic Role:user messages from the chat display so agent-mode internals no longer leak into the desktop/TUI transcript. Closes esengine#3738, Closes esengine#3720.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Core agent loop (internal/agent, internal/control) desktop Wails desktop app (desktop/**) tui Terminal UI / CLI (internal/cli, internal/control) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: agent模式更改的注入提示词泄漏到聊天框内

1 participant