fix(ui): hide async exec system events and heartbeat acks from chat transcript#69366
fix(ui): hide async exec system events and heartbeat acks from chat transcript#69366srinivaspavan9 wants to merge 1 commit into
Conversation
Greptile SummaryThis PR hides async exec system events and
Confidence Score: 4/5PR is close to correct but has one P1 inconsistency in the renderer that can silently drop real relay content. The detection and hide logic in chat.ts is correct. The grouped-render.ts strip call uses the wrong default maxAckChars (300) compared to the 0 used everywhere else, causing relay messages under 300 chars to be invisibly suppressed despite passing the history filter. ui/src/ui/chat/grouped-render.ts — the stripHeartbeatToken call at line 1214 needs maxAckChars: 0 Prompt To Fix All With AIThis is a comment left during a code review.
Path: ui/src/ui/chat/grouped-render.ts
Line: 1214-1215
Comment:
**Short relay messages silently suppressed by 300-char default**
`mode: "heartbeat"` with no `maxAckChars` defaults to `DEFAULT_HEARTBEAT_ACK_MAX_CHARS = 300`, meaning any assistant message whose remaining text after stripping `HEARTBEAT_OK` is ≤ 300 chars will yield `shouldSkip: true` here. `shouldHideHistoryMessage` uses `ackMaxChars: 0` (only suppresses truly empty-after-strip messages), so a relay message like `"You have 3 unread urgent emails. HEARTBEAT_OK"` (32 chars post-strip) passes the history filter but is then silently dropped in the renderer — contradicting the stated PR goal of showing relay messages' human-readable content.
```suggestion
const s = stripHeartbeatToken(markdownBase, { mode: "heartbeat", maxAckChars: 0 });
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(ui): hide async exec system events a..." | Re-trigger Greptile |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7c8cb2c71a
ℹ️ 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".
7c8cb2c to
0d89817
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0d898177df
ℹ️ 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".
| const EXEC_INJECTION_PREFIX_RE = | ||
| /^System \(untrusted\): \[.+?\] Exec (completed|failed|finished)\b/i; |
There was a problem hiding this comment.
Do not hide user messages by exec-event text pattern alone
This regex is treated as a definitive signal for internal exec injections, but shouldHideHistoryMessage applies it to all user history entries and inbound sanitization already rewrites line-leading System: to System (untrusted): (src/auto-reply/reply/inbound-text.ts), so a legitimate user message that starts with System: [..] Exec completed/failed/finished ... will be transformed to this shape and then silently removed from the UI transcript on history load. That is user-visible data loss and needs an additional non-user-controllable discriminator before filtering.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
This is a relly good comment, the current fix only acts like a mere bandaid. I have a new commit which fixes the main root cause
|
Related work from PRtags group Title: Open PR duplicate: Control UI async exec/system transcript leak
|
|
Closing this as implemented after Codex review. Current What I checked:
So I’m closing this as already implemented rather than keeping a duplicate issue open. Codex Review notes: reviewed against 80b6da72f5f0; fix evidence: commit 3f63ba8fd808. |
…ents Adds an optional `audience: 'internal' | 'user-facing'` field to SystemEvent, defaulting to 'user-facing' (full backward-compat). Internal-audience events are wrapped at the consumer integration point (drainFormattedSystemEvents) in the existing INTERNAL_RUNTIME_CONTEXT_BEGIN/END delimiters with the canonical header lines from formatAgentInternalEventsForPrompt — the agent runtime sees the content as runtime context but every user-facing surface strips it via the already-installed stripInternalRuntimeContext consumers (sanitize-user-facing-text.ts, memory-host-sdk session-files, history readers, Control UI extract). This mirrors the producer-side runtime-context delimiter pattern landed in e918e5f (openclaw#71761) and 6e985a4 (webchat runtime context) — no new persistence shape, no consumer-side filter (avoids the audit-bypass smell flagged on openclaw#69217), no display-layer per-surface patch (avoids the rejection pattern from openclaw#69366). Scope is intentionally tight. This primitive is the hidden runtime-context lane only; it is NOT a delivery-routing primitive. Events with a positive user delivery contract (exec completion via notifyOnExit, cron payloads, heartbeat acks) are not migrated — those have heartbeat-driven explicit-relay paths (buildExecEventPrompt / buildCronEventPrompt) plus tactical producer-side skips like bd60df3. Migrated (one caller): - queueCronAwarenessSystemEvent in src/cron/isolated-agent/delivery-dispatch.ts. Main-session reflection of an isolated agent run that already delivered to the user via its own channel (gated on `delivered === true`). Pure awareness, no user delivery contract. Tests cover: default audience, round-trip preservation, audience equality (consumeSystemEventEntries prefix-match respects audience), wrap-on-drain shape with canonical header, mixed user-facing+internal ordering, the user-facing-only and internal-only edge cases, end-to-end strip via stripInternalRuntimeContext, and adversarial delimiter-token escape. Addresses part of openclaw#69492 (system-event-shape consumer leakage). Leaves the user-deliverable noise question (exec completion, cron payloads, heartbeat acks) open for case-by-case follow-ups matching the maintainer's revealed pattern (bd60df3, 3f63ba8). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ents Adds an optional `audience: 'internal' | 'user-facing'` field to SystemEvent, defaulting to 'user-facing' (full backward-compat). Internal-audience events are wrapped at the consumer integration point (drainFormattedSystemEvents) in the existing INTERNAL_RUNTIME_CONTEXT_BEGIN/END delimiters with the canonical header lines from formatAgentInternalEventsForPrompt — the agent runtime sees the content as runtime context but every user-facing surface strips it via the already-installed stripInternalRuntimeContext consumers (sanitize-user-facing-text.ts, memory-host-sdk session-files, history readers, Control UI extract). This mirrors the producer-side runtime-context delimiter pattern landed in e918e5f (openclaw#71761) and 6e985a4 (webchat runtime context) — no new persistence shape, no consumer-side filter (avoids the audit-bypass smell flagged on openclaw#69217), no display-layer per-surface patch (avoids the rejection pattern from openclaw#69366). Scope is intentionally tight. This primitive is the hidden runtime-context lane only; it is NOT a delivery-routing primitive. Events with a positive user delivery contract (exec completion via notifyOnExit, cron payloads, heartbeat acks) are not migrated — those have heartbeat-driven explicit-relay paths (buildExecEventPrompt / buildCronEventPrompt) plus tactical producer-side skips like bd60df3. Migrated (one caller): - queueCronAwarenessSystemEvent in src/cron/isolated-agent/delivery-dispatch.ts. Main-session reflection of an isolated agent run that already delivered to the user via its own channel (gated on `delivered === true`). Pure awareness, no user delivery contract. Tests cover: default audience, round-trip preservation, audience equality (consumeSystemEventEntries prefix-match respects audience), wrap-on-drain shape with canonical header, mixed user-facing+internal ordering, the user-facing-only and internal-only edge cases, end-to-end strip via stripInternalRuntimeContext, and adversarial delimiter-token escape. Addresses part of openclaw#69492 (system-event-shape consumer leakage). Leaves the user-deliverable noise question (exec completion, cron payloads, heartbeat acks) open for case-by-case follow-ups matching the maintainer's revealed pattern (bd60df3, 3f63ba8). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ents Adds an optional `audience: 'internal' | 'user-facing'` field to SystemEvent, defaulting to 'user-facing' (full backward-compat). Internal-audience events are wrapped at the consumer integration point (drainFormattedSystemEvents) in the existing INTERNAL_RUNTIME_CONTEXT_BEGIN/END delimiters with the canonical header lines from formatAgentInternalEventsForPrompt — the agent runtime sees the content as runtime context but every user-facing surface strips it via the already-installed stripInternalRuntimeContext consumers (sanitize-user-facing-text.ts, memory-host-sdk session-files, history readers, Control UI extract). This mirrors the producer-side runtime-context delimiter pattern landed in e918e5f (openclaw#71761) and 6e985a4 (webchat runtime context) — no new persistence shape, no consumer-side filter (avoids the audit-bypass smell flagged on openclaw#69217), no display-layer per-surface patch (avoids the rejection pattern from openclaw#69366). Scope is intentionally tight. This primitive is the hidden runtime-context lane only; it is NOT a delivery-routing primitive. Events with a positive user delivery contract (exec completion via notifyOnExit, cron payloads, heartbeat acks) are not migrated — those have heartbeat-driven explicit-relay paths (buildExecEventPrompt / buildCronEventPrompt) plus tactical producer-side skips like bd60df3. Migrated (one caller): - queueCronAwarenessSystemEvent in src/cron/isolated-agent/delivery-dispatch.ts. Main-session reflection of an isolated agent run that already delivered to the user via its own channel (gated on `delivered === true`). Pure awareness, no user delivery contract. Tests cover: default audience, round-trip preservation, audience equality (consumeSystemEventEntries prefix-match respects audience), wrap-on-drain shape with canonical header, mixed user-facing+internal ordering, the user-facing-only and internal-only edge cases, end-to-end strip via stripInternalRuntimeContext, and adversarial delimiter-token escape. Addresses part of openclaw#69492 (system-event-shape consumer leakage). Leaves the user-deliverable noise question (exec completion, cron payloads, heartbeat acks) open for case-by-case follow-ups matching the maintainer's revealed pattern (bd60df3, 3f63ba8). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ents Adds an optional `audience: 'internal' | 'user-facing'` field to SystemEvent, defaulting to 'user-facing' (full backward-compat). Internal-audience events are wrapped at the consumer integration point (drainFormattedSystemEvents) in the existing INTERNAL_RUNTIME_CONTEXT_BEGIN/END delimiters with the canonical header lines from formatAgentInternalEventsForPrompt — the agent runtime sees the content as runtime context but every user-facing surface strips it via the already-installed stripInternalRuntimeContext consumers (sanitize-user-facing-text.ts, memory-host-sdk session-files, history readers, Control UI extract). This mirrors the producer-side runtime-context delimiter pattern landed in e918e5f (openclaw#71761) and 6e985a4 (webchat runtime context) — no new persistence shape, no consumer-side filter (avoids the audit-bypass smell flagged on openclaw#69217), no display-layer per-surface patch (avoids the rejection pattern from openclaw#69366). Scope is intentionally tight. This primitive is the hidden runtime-context lane only; it is NOT a delivery-routing primitive. Events with a positive user delivery contract (exec completion via notifyOnExit, cron payloads, heartbeat acks) are not migrated — those have heartbeat-driven explicit-relay paths (buildExecEventPrompt / buildCronEventPrompt) plus tactical producer-side skips like bd60df3. Migrated (one caller): - queueCronAwarenessSystemEvent in src/cron/isolated-agent/delivery-dispatch.ts. Main-session reflection of an isolated agent run that already delivered to the user via its own channel (gated on `delivered === true`). Pure awareness, no user delivery contract. Tests cover: default audience, round-trip preservation, audience equality (consumeSystemEventEntries prefix-match respects audience), wrap-on-drain shape with canonical header, mixed user-facing+internal ordering, the user-facing-only and internal-only edge cases, end-to-end strip via stripInternalRuntimeContext, and adversarial delimiter-token escape. Addresses part of openclaw#69492 (system-event-shape consumer leakage). Leaves the user-deliverable noise question (exec completion, cron payloads, heartbeat acks) open for case-by-case follow-ups matching the maintainer's revealed pattern (bd60df3, 3f63ba8). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ents Adds an optional `audience: 'internal' | 'user-facing'` field to SystemEvent, defaulting to 'user-facing' (full backward-compat). Internal-audience events are wrapped at the consumer integration point (drainFormattedSystemEvents) in the existing INTERNAL_RUNTIME_CONTEXT_BEGIN/END delimiters with the canonical header lines from formatAgentInternalEventsForPrompt — the agent runtime sees the content as runtime context but every user-facing surface strips it via the already-installed stripInternalRuntimeContext consumers (sanitize-user-facing-text.ts, memory-host-sdk session-files, history readers, Control UI extract). This mirrors the producer-side runtime-context delimiter pattern landed in e918e5f (openclaw#71761) and 6e985a4 (webchat runtime context) — no new persistence shape, no consumer-side filter (avoids the audit-bypass smell flagged on openclaw#69217), no display-layer per-surface patch (avoids the rejection pattern from openclaw#69366). Scope is intentionally tight. This primitive is the hidden runtime-context lane only; it is NOT a delivery-routing primitive. Events with a positive user delivery contract (exec completion via notifyOnExit, cron payloads, heartbeat acks) are not migrated — those have heartbeat-driven explicit-relay paths (buildExecEventPrompt / buildCronEventPrompt) plus tactical producer-side skips like bd60df3. Migrated (one caller): - queueCronAwarenessSystemEvent in src/cron/isolated-agent/delivery-dispatch.ts. Main-session reflection of an isolated agent run that already delivered to the user via its own channel (gated on `delivered === true`). Pure awareness, no user delivery contract. Tests cover: default audience, round-trip preservation, audience equality (consumeSystemEventEntries prefix-match respects audience), wrap-on-drain shape with canonical header, mixed user-facing+internal ordering, the user-facing-only and internal-only edge cases, end-to-end strip via stripInternalRuntimeContext, and adversarial delimiter-token escape. Addresses part of openclaw#69492 (system-event-shape consumer leakage). Leaves the user-deliverable noise question (exec completion, cron payloads, heartbeat acks) open for case-by-case follow-ups matching the maintainer's revealed pattern (bd60df3, 3f63ba8). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ents Adds an optional `audience: 'internal' | 'user-facing'` field to `SystemEvent`, defaulting to `'user-facing'` (full backward-compat for every existing caller). Internal-audience events are wrapped at the consumer integration point (`drainFormattedSystemEvents` in session-system-events.ts) in the existing `INTERNAL_RUNTIME_CONTEXT_BEGIN/END` delimiters with the canonical header lines from `formatAgentInternalEventsForPrompt`. The agent runtime sees the content as runtime context; user-facing surfaces strip it via the already-installed `stripInternalRuntimeContext` consumers (`sanitize-user-facing-text.ts`, `memory-host-sdk/host/session-files.ts`, `agents/internal-events.ts`, history readers, Control UI extract). Mirrors the producer-side runtime-context delimiter pattern landed in `e918e5f75c` (openclaw#71761) and `6e985a421d` (webchat runtime context). No new persistence shape, no consumer-side filter (avoids the audit-bypass concern flagged on openclaw#69217), no display-layer per-surface patch (avoids the rejection pattern from openclaw#69366). Heartbeat-path additions: `selectGenericSystemEvents` keeps audience: "internal" events queued when the drain is invoked from a heartbeat reply (whose prompt envelope discards `systemEventBlocks`), so the wrapped runtime-context block is not silently consumed before the next regular reply turn that actually carries it through to the model. The heartbeat exec/cron-prompt selectors also skip internal events so they don't get classified as user-facing relay payloads. `compactSystemEvent` bypasses heartbeat-noise filters ("reason periodic", "Read HEARTBEAT.md", "heartbeat poll/wake") for audience: "internal" events — those go through the wrap-on-drain path and never reach a user-facing surface, so filtering them after consumption would silently drop the event (same no-consumer hole class as the exec-shape filter). Migrated one producer (the canonical justification for the lane): - `queueCronAwarenessSystemEvent` in `src/cron/isolated-agent/delivery-dispatch.ts`. This event is queued onto the main session only after the isolated agent run has already delivered to the user via its own channel (gated on `delivered === true` and `shouldQueueCronAwareness`). The main session needs hidden runtime context so the next turn knows the cron landed, not a user-visible `System: ...` bubble. Scope is intentionally tight. This primitive is the hidden runtime-context lane only; it is NOT a delivery-routing primitive. Events with a positive user delivery contract (exec completion via `notifyOnExit`, cron payloads, heartbeat acks) are not migrated — those have heartbeat-driven explicit-relay paths (`buildExecEventPrompt` / `buildCronEventPrompt`) plus tactical producer-side skips like `bd60df3e53`. Marking them internal would suppress delivery on regular reply turns where the model is instructed to keep wrapped content private. Tests cover: default audience, round-trip preservation, audience equality (consumeSystemEventEntries prefix-match respects audience), wrap-on-drain shape with canonical header, mixed user-facing+internal ordering, the user-facing-only and internal-only edge cases, end-to-end strip via stripInternalRuntimeContext, adversarial delimiter-token escape, heartbeat-path audience: "internal" queueing semantics, and exec-shaped internal events bypassing the text-shape filter. Addresses part of openclaw#69492 (system-event-shape consumer leakage; umbrella openclaw#69208 Track B). Leaves the user-deliverable noise question (exec completion, cron payloads, heartbeat acks) open for case-by-case follow-ups matching the maintainer's revealed pattern (`bd60df3e53`, `3f63ba8fd808`). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
System (untrusted): [...] Exec completed...and (2) a pureHEARTBEAT_OKassistant ack bubble.heartbeat-filter.ts, created a browser-safeheartbeat-client.tsreimplementation for the UI bundle (avoids pulling in Node.js built-ins viasrc/utils.ts), extendedshouldHideHistoryMessageinchat.tsto hide both message types, and addedHEARTBEAT_OKstripping to the renderer ingrouped-render.ts.Change Type
Scope
Linked Issue/PR
Root Cause
user-role messages directly into the session (prefixedSystem (untrusted): [...] Exec completed...). The Control UI had no filter for these, so they rendered as if the user had sent them. Similarly, pureHEARTBEAT_OKassistant acks had no stripping logic, so they appeared as assistant bubbles.shouldHideHistoryMessageonly filteredisAssistantSilentReplyand synthetic transcript repair tool results — no awareness of heartbeat-injected user messages or HEARTBEAT_OK acks.System (untrusted):prefix is unique to heartbeat-injected messages (inbound sanitization rewrites it for real user input), so it is a safe and reliable signal to filter on.Regression Test Plan
src/auto-reply/heartbeat-filter.test.tsisExecEventInjectionMessagecorrectly matches all three exec event variants (completed,failed,finished), both string and array content, and does not match regular user messages or heartbeat prompts.User-visible / Behavior Changes
System (untrusted): [...] Exec completed/failed/finished ...messages no longer appear as user chat bubbles in the Control UI.HEARTBEAT_OKassistant acks no longer appear as assistant chat bubbles.HEARTBEAT_OK(e.g. "Command finished... HEARTBEAT_OK") have the token stripped and show only the human-readable content.Diagram
Security Impact
Repro + Verification
Environment
Steps
run this shell command asynchronously: sleep 3 && echo helloExpected
No
System (untrusted):user bubble. NoHEARTBEAT_OKassistant bubble.Actual (before fix)
A fake "You" bubble appears:
System (untrusted): [timestamp] Exec completed (session, code 0) :: helloA
HEARTBEAT_OKassistant bubble may also appear.Evidence
heartbeat-filter.test.tsHuman Verification
System (untrusted)user bubbles, noHEARTBEAT_OKassistant bubbles in any scenarioHEARTBEAT_OKshow only the human-readable contentpnpm build && pnpm check && pnpm testall green (798 tests, 68 test files)Review Conversations
Compatibility / Migration
Risks and Mitigations
EXEC_INJECTION_PREFIX_REpattern too broad — could accidentally hide legitimate user messages.System (untrusted):prefix is actively rewritten by inbound sanitization for real user input, so it cannot appear in genuine messages. Pattern is tightly anchored to that prefix plus a timestamp.heartbeat-client.tsdrifts fromheartbeat-filter.ts/heartbeat.tssource of truth.