fix(ui): filter leaked control ui transcript rows#67036
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 69bf4049b1
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (lower.startsWith("system:") || lower.startsWith("system (untrusted):")) { | ||
| return true; |
There was a problem hiding this comment.
Restrict system-prefix filter to leaked exec rows
This guard now drops any message whose text starts with System: or System (untrusted):, regardless of role or whether it is an internal exec-status leak. That means legitimate user/assistant content like System: reboot checklist... will disappear from the transcript, which is a user-visible data-loss regression for normal chat inputs. The check should be narrowed to the known leaked signature (for example, require the exec-status marker and/or the expected internal role metadata) instead of hiding all System:-prefixed text.
Useful? React with 👍 / 👎.
Greptile SummaryThis PR adds two-layer filtering to prevent leaked internal transcript rows (e.g., Confidence Score: 5/5Safe to merge; the two-layer filtering correctly handles the leaked-row scenarios described, and all edge cases are covered by new regression tests. The only finding is a P2 style/guard asymmetry in the view filter — intentional for defence-in-depth but subtly over-broad for user-role messages. No P0/P1 defects were found, so the score stays at 5. ui/src/ui/views/chat.ts — the shouldHideRenderedHistoryMessage role-guard gap noted above. Prompt To Fix All With AIThis is a comment left during a code review.
Path: ui/src/ui/views/chat.ts
Line: 1747-1749
Comment:
**View filter can silence legitimate user messages starting with "system:"**
`shouldHideRenderedHistoryMessage` has no role guard for the `"system:"` / `"system (untrusted):"` prefix check. A user message whose text starts with `"system:"` (case-insensitive) — e.g., `"System: please reset the session"` — would survive the controller filter (which correctly gates on `role === "system"`) but then be silently dropped here at render time.
The controller's `isLeakedInternalHistoryMessage` demonstrates the safer pattern: role-gate the prefix check while keeping the `sender (untrusted metadata):` branch role-agnostic for the inconsistent-role scenario. Excluding `"user"` role here would preserve the defence-in-depth intent without the over-filtering risk:
```suggestion
const msg = message as Record<string, unknown>;
const role = typeof msg.role === "string" ? msg.role.toLowerCase() : "";
if (role !== "user" && (lower.startsWith("system:") || lower.startsWith("system (untrusted):"))) {
return true;
}
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(ui): filter leaked control ui transc..." | Re-trigger Greptile |
| if (lower.startsWith("system:") || lower.startsWith("system (untrusted):")) { | ||
| return true; | ||
| } |
There was a problem hiding this comment.
View filter can silence legitimate user messages starting with "system:"
shouldHideRenderedHistoryMessage has no role guard for the "system:" / "system (untrusted):" prefix check. A user message whose text starts with "system:" (case-insensitive) — e.g., "System: please reset the session" — would survive the controller filter (which correctly gates on role === "system") but then be silently dropped here at render time.
The controller's isLeakedInternalHistoryMessage demonstrates the safer pattern: role-gate the prefix check while keeping the sender (untrusted metadata): branch role-agnostic for the inconsistent-role scenario. Excluding "user" role here would preserve the defence-in-depth intent without the over-filtering risk:
| if (lower.startsWith("system:") || lower.startsWith("system (untrusted):")) { | |
| return true; | |
| } | |
| const msg = message as Record<string, unknown>; | |
| const role = typeof msg.role === "string" ? msg.role.toLowerCase() : ""; | |
| if (role !== "user" && (lower.startsWith("system:") || lower.startsWith("system (untrusted):"))) { | |
| return true; | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/src/ui/views/chat.ts
Line: 1747-1749
Comment:
**View filter can silence legitimate user messages starting with "system:"**
`shouldHideRenderedHistoryMessage` has no role guard for the `"system:"` / `"system (untrusted):"` prefix check. A user message whose text starts with `"system:"` (case-insensitive) — e.g., `"System: please reset the session"` — would survive the controller filter (which correctly gates on `role === "system"`) but then be silently dropped here at render time.
The controller's `isLeakedInternalHistoryMessage` demonstrates the safer pattern: role-gate the prefix check while keeping the `sender (untrusted metadata):` branch role-agnostic for the inconsistent-role scenario. Excluding `"user"` role here would preserve the defence-in-depth intent without the over-filtering risk:
```suggestion
const msg = message as Record<string, unknown>;
const role = typeof msg.role === "string" ? msg.role.toLowerCase() : "";
if (role !== "user" && (lower.startsWith("system:") || lower.startsWith("system (untrusted):"))) {
return true;
}
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7c456a3614
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| role === "system" && | ||
| (lower.startsWith("system:") || lower.startsWith("system (untrusted):") || hasExecStatus) | ||
| ) { |
There was a problem hiding this comment.
Align load-time leak filter with render-time leak guard
The new history filter only removes System:/System (untrusted): rows when role === "system", but the render path hides those prefixes for any role. Since leaked rows are known to arrive with inconsistent roles (the new view test uses role: "assistant"), those entries stay in state.chatMessages after loadChatHistory and can still surface in consumers of raw history such as chat export (ui/src/ui/app.ts:544 uses exportChatMarkdown(this.chatMessages, ...)). This creates a user-visible leak path where hidden rows reappear in exports; the ingestion and render predicates should be consistent (or export should apply the same scrubber).
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1f5d562af0
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (hasExecStatus && (lower.startsWith("system:") || lower.startsWith("system (untrusted):"))) { | ||
| return true; |
There was a problem hiding this comment.
Narrow system-row filter to avoid hiding normal chat text
isLeakedInternalHistoryMessage drops any row that starts with System: whenever the text contains broad tokens like exec failed/exec completed, and that shared predicate is used by history load, render, and export paths. This means a legitimate message such as System: exec failed after deploy, retrying will be removed from the transcript after refresh/export even though it is not leaked internal status content. Fresh evidence compared with the earlier review thread: the added hasLeakedExecStatus gate still matches generic wording and does not require the known leaked timestamp/signature shape.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f6d1450a59
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (hasLeakedSenderMetadataExecStatus(lower)) { | ||
| return true; | ||
| } | ||
| return lower.startsWith(normalizeLowercaseStringOrEmpty(LEAKED_SESSION_RESET_PROMPT_PREFIX)); |
There was a problem hiding this comment.
Narrow session-reset leak filter to internal prompt shape
The new startsWith check for LEAKED_SESSION_RESET_PROMPT_PREFIX will hide any message that begins with this sentence, regardless of role or whether it is actually an internal leak. In practice, if a user or assistant legitimately quotes that reset instruction text (for debugging, docs discussion, or prompt analysis), the message is silently removed from history/render/export, which is user-visible transcript data loss. This predicate should require additional leak-specific markers (for example the known appended startup-prompt structure) instead of matching only the shared prefix.
Useful? React with 👍 / 👎.
|
Related work from PRtags group Title: Open PR duplicate: Control UI async exec/system transcript leak
|
|
Closing this as implemented after Codex automated review. Current main already fixes the observable Control UI transcript leak through the shipped runtime/transcript path rather than this PR's UI-only row filter. Exec/heartbeat wakeups are treated as heartbeat/system-event runs, their prompt body is excluded from the visible transcript prompt, and current chat history/live-chat projection strips heartbeat acknowledgements and internal runtime context. The linked bug is documented as fixed in v2026.4.24. Best possible solution: Close this PR and keep the shipped runtime/transcript implementation on main. Any remaining concern about historical leaked rows should be handled as a focused legacy transcript repair or session-history projection follow-up, not by merging this broad UI-only filter. What I checked:
So I’m closing this as already implemented rather than keeping a duplicate issue open. Codex Review notes: model gpt-5.5, reasoning high; reviewed against 7c0fdae9b95b; fix evidence: release v2026.4.24, commit cbcfdf62c729. |
Summary
System:/System (untrusted):exec-status lines and sender-envelope noise as normal chat messages.controllers/chat.ts, added a render-time guard inviews/chat.ts, and added regression tests for both layers.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Root Cause (if applicable)
Regression Test Plan (if applicable)
ui/src/ui/controllers/chat.test.tsui/src/ui/views/chat.test.tsSystem (untrusted): ... Exec completed ...rows are dropped from loaded historyUser-visible / Behavior Changes
Diagram (if applicable)
Security Impact (required)
Yes, explain risk + mitigation:Repro + Verification
Environment
Steps
System:/System (untrusted):exec-status rows or sender-envelope noise appear as normal chat bubbles.Expected
Actual
Evidence
Attach at least one:
Human Verification (required)
System (untrusted)/ exec rows after refreshReview Conversations
Compatibility / Migration
Risks and Mitigations