You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Under umbrella #69208 Track B ("Delivery-mirror and consumer-path leakage"), the 2026-04-20 cleanup comment names #39469 as the Track B canonical and scopes the supporting issues (#38061, #33263, #5964) to assistant-message-shape artifacts. PR #69217 is the active Track B fix for that slice — it adds isTranscriptOnlyOpenClawAssistantMessage in src/config/sessions/transcript.ts and wires it into chat.history, sessions.get, and the sessions history HTTP reader.
The same consumer-path boundary also leaks system-event-shape artifacts — enqueueSystemEvent outputs from the exec runtime, cron, and heartbeat flows. These events carry a different object shape (no role, no provider, no model; their fields are text, ts, contextKey, deliveryContext, trusted), so #69217's predicate does not filter them. There is no canonical Track B issue for this shape today, and the fixes currently in flight for specific symptoms are per-surface or per-channel.
This issue is intended as the system-event-shape canonical under Track B, and proposes a per-event audience: 'internal' | 'user-facing' | 'auto' classification as the underlying primitive. The consumer-path filter would follow as a sibling predicate to #69217's, in the same file.
Status update — 2026-05-05
Posted as a status amendment on the original 2026-04-20 issue. The original analysis below is preserved for historical context. Many of the referenced cross-refs have changed state since filing; this section documents the deltas and the live implementation track.
Track B canonical (#39469) — open → closed 2026-04-24 as already-implemented via e95efa4373 fix(sessions): dedupe redundant delivery mirrors and follow-up d842ec4179 fix: tighten delivery mirror dedupe (#67185), both shipped in v2026.4.15. That fix is in the transcript-append layer (src/config/sessions/transcript.ts): before appending a provider:"openclaw", model:"delivery-mirror" assistant message, the writer trims visible text and reuses the latest assistant entry when the trimmed text matches. Important framing: that closure addresses assistant-message-shape Track B (the duplicate-mirror symptom). It does not touch the system-event-shape lane that this issue's subject is about — system events have a different object shape and do not flow through appendAssistantMessageToSessionTranscript. The system-event-shape lane remains unaddressed at the canonical layer.
Track B canonical-adjacent PRs (state at filing → now)
Hide transcript-only OpenClaw history artifacts #69217 Hide transcript-only OpenClaw history artifacts — open → closed 2026-04-26 (NOT merged). Likely closed because e95efa4373 / d842ec4179 resolved the symptom at the producer layer instead of via consumer-side filtering. The audit-bypass concern flagged in review on this PR is the reason the implementation track for the system-event-shape lane (feat(events): add audience field for hidden runtime-context system events #72201, see below) avoids a consumer-side filter and uses wrap-on-drain instead.
Adjacent reshape that affects this PR's design (not a supersession)
9180173f9a fix: preserve exec event routing and sanitize tool XML (post-v2026.5.4). Introduces selectGenericSystemEvents + consumeSelectedSystemEventEntries so exec-completion events stay queued for the heartbeat path and only generic events are drained at the autoreply integration point. This reshapes the surface feat(events): add audience field for hidden runtime-context system events #72201 hooks into but does not replace it — the audience split still applies to the generic events that are drained. feat(events): add audience field for hidden runtime-context system events #72201 is rebased on top of this and now uses consumeSelectedSystemEventEntries(...) rather than drainSystemEventEntries(...).
Direct system-event-shape reports (original A list)
feat(events): add audience field for hidden runtime-context system events #72201feat(events): add audience field for hidden runtime-context system events is the live implementation slice from this proposal. Currently OPEN, rebased onto current main (HEAD e68ce9353c). Adds optional SystemEvent.audience and wraps internal-audience events at drainFormattedSystemEvents in the existing INTERNAL_RUNTIME_CONTEXT_* delimiters; migrates exactly one producer (queueCronAwarenessSystemEvent) for review tractability. No batch migration committed.
Self-correction on the original "Consumer-path filter" section
That section proposed a sibling predicate to #69217's isTranscriptOnlyOpenClawAssistantMessage. With #69217 closed without merge AND the v2026.4.15 transcript-dedup fix landing producer-side, the consumer-side predicate approach is no longer actively pursued by this issue — #72201's wrap-on-drain primitive supplants it. The original "Proposed Shape" → "Consumer-path filter" subsection is preserved below for context but should be read as superseded by the wrap-on-drain design.
Why This Issue Exists
Several open reports describe system-event text landing in user-visible chat transcripts across multiple channels. The individual reports differ in surface and exact symptom, but the shared pattern is:
An internal producer (maybeNotifyOnExit, heartbeat relay, cron) calls enqueueSystemEvent with a SystemEvent payload.
That event is consumed by downstream readers (chat.history, sessions.get, sessions history HTTP, channel dispatchers) that have no way to distinguish "this event was meant for internal agent awareness" from "this event should reach the user."
The event is rendered, persisted, or relayed as if it were user-facing conversational content.
Existing visibility controls in the runtime (tools.exec.notifyOnExit, visibility.showAlerts, heartbeat.target, suppressToolErrors, ANNOUNCE_SKIP_TOKEN) are each too coarse to express this distinction at event granularity. Prior work that closed similar reports by adding boolean flags (e.g., #22823, #20193) has not held up in practice — #19894 showed that notifyOnExit:false + suppressToolErrors still leaks sub-agent exec failures, which suggests that boolean notify flags are not the right primitive.
The ExecToolDefaults.trigger field already carries 'heartbeat' / 'cron' / caller context through runExecProcess, but maybeNotifyOnExit never reads it. That signal is present in the runtime today but unused for classification.
B. Cross-cuts assistant-message-shape (addressed by #69217)
Internal exec/heartbeat events leak into visible Control UI chat transcript #66814 reports both system-event text and heartbeat-followup assistant-message leakage. The bundle-inspection analysis in that issue enumerates bash-tools.exec-runtime-*, heartbeat-runner-*, session-system-events-*, sessions-history-http-* as leak paths. Some symptoms should be covered by Hide transcript-only OpenClaw history artifacts #69217 once it lands; residual system-event text is this issue's responsibility. Worth re-reading with the reporter after both changes are in place.
C. Cross-cuts Track A (duplicate persistence — canonical #66443)
These overlap with Track A at the persistence layer. An event-level audience classification would prevent persistence for internal events, which partially addresses them from this direction; the Track A idempotency work covers the remainder.
Hide transcript-only OpenClaw history artifacts #69217 — assistant-message-shape consumer-path filter (isTranscriptOnlyOpenClawAssistantMessage). Track B canonical-adjacent; this issue proposes a system-event-shape peer predicate in the same file.
SystemEvent (src/infra/system-events.ts) has text, ts, contextKey, deliveryContext, trusted — no field that expresses the producer's intent about whether the event should reach the user. deliveryContext routes where an event goes; it does not say whether the event should appear on any user-facing surface.
ExecToolDefaults.trigger (src/agents/bash-tools.exec-types.ts:12) already carries 'heartbeat' / 'cron' / caller context through to the runtime. maybeNotifyOnExit in src/agents/bash-tools.exec-runtime.ts:279–308 does not consult it when deciding what to emit. The signal is present, unused.
2. Consumer-path write-through to visible transcripts
buildExecEventPrompt({ deliverToUser: false }) in src/infra/heartbeat-events-filter.ts:41–54 produces text that literally says "Handle the result internally. Do not relay it to the user unless explicitly requested."#68508 shows this exact directive text landing in visible chat transcripts, which suggests the filter is happening at the agent-instruction level but not at the transcript-persistence level. The pattern #69217 establishes for assistant-message-shape artifacts — a shared predicate called at consumer-path boundaries — applies directly to the system-event shape as well.
3. Per-surface patches are running faster than the underlying problem shrinks
The three Track B PRs filed in the last 48 hours each address a different slice (artifact shape, one UI surface, one misreferenced flag). They collectively close roughly one of the open reports in the system-event cluster. The remaining reports span Matrix, Discord, Telegram, Teams, Mattermost, TUI, and webchat; without an event-level primitive, each new surface needs its own filter and each format change (timezone prefix, text wording, output length) re-opens existing filters. An event-level audience field is the layer at which this can be closed consistently.
4. Boolean notify flags are structurally insufficient
Closed-issue precedent #19894 showed that notifyOnExit:false + suppressToolErrors compositions still leak sub-agent exec failures. Adding more top-level booleans has not held. An enum field on the event itself is a different concept, not another boolean.
Proposed Shape
This is a proposal, not a committed design. Happy to revise per maintainer preference on any of these.
Core primitive
Extend SystemEvent with an optional audience field:
Default is 'auto'. No existing call site has to change.
'user-facing' — reach the user when routing and visibility allow.
'internal' — agent/session/log awareness only. Heartbeat may read; agent may reason; appears in session state. Does not flow to user-facing chat surfaces. Not written to chat.history as a visible entry.
'auto' — defer to downstream (current behavior).
Spawn-time intent
Extend ExecToolDefaults and ProcessSession with completionAudience?: SystemEventAudience. Derive at maybeNotifyOnExit:
completionAudience explicit from defaults? → use it
else session.trigger ∈ {'heartbeat','cron','subagent'}? → 'internal'
else → 'user-facing'
This activates the existing-but-unused trigger field. Matches #66460's reporter's explicit fix proposal, generalized beyond cron.
Relay-time honoring
In src/infra/heartbeat-runner.ts (canRelayToUser computation around line 812):
Wired into the same consumer-path boundaries #69217 touches. The exact call-site shape — two peer predicates called per consumer, or one combined dispatcher that inspects entry shape — is a maintainer preference. Happy to match whatever #69217 settles on.
Config fallback
Optional per-channel default for unspecified events:
Whether the Track B scope should be expanded to include system-event-shape artifacts, or whether this should be treated as a separate track.
Whether the per-event audience primitive is the right direction, versus alternatives such as extending per-channel showAlerts to be per-trigger, or extending trusted to be a tri-state visibility field.
Naming — audience vs visibility on the event; completionAudience vs completionVisibility on defaults. Draft uses audience to reduce conflation with the existing visibility.showAlerts channel-level knob.
Consumer-gate shape — two peer predicates or a single combined dispatcher.
Open issues in the list above with unique reproductions or unique affected surfaces should stay open until their exact symptom is verified fixed. This issue is meant to consolidate analysis and design, not to auto-close linked issues.
If #69366 merges and closes #68992 for Control UI only, non-Control-UI reports of the same symptom should stay open and be tracked here until the backend fix lands.
Happy to contribute an implementation PR along these lines if the direction is accepted, following the same iterative bot-review cadence as #67508.
AI-assisted research: cross-indexed open and closed issues, traced the event pipeline in 2026.4.19-beta.2, and drafted this issue with Claude Code (Claude Opus 4.7, 1M context). Fully reviewed.
Summary
Under umbrella #69208 Track B ("Delivery-mirror and consumer-path leakage"), the 2026-04-20 cleanup comment names #39469 as the Track B canonical and scopes the supporting issues (#38061, #33263, #5964) to assistant-message-shape artifacts. PR #69217 is the active Track B fix for that slice — it adds
isTranscriptOnlyOpenClawAssistantMessageinsrc/config/sessions/transcript.tsand wires it intochat.history,sessions.get, and the sessions history HTTP reader.The same consumer-path boundary also leaks system-event-shape artifacts —
enqueueSystemEventoutputs from the exec runtime, cron, and heartbeat flows. These events carry a different object shape (norole, noprovider, nomodel; their fields aretext,ts,contextKey,deliveryContext,trusted), so #69217's predicate does not filter them. There is no canonical Track B issue for this shape today, and the fixes currently in flight for specific symptoms are per-surface or per-channel.This issue is intended as the system-event-shape canonical under Track B, and proposes a per-event
audience: 'internal' | 'user-facing' | 'auto'classification as the underlying primitive. The consumer-path filter would follow as a sibling predicate to #69217's, in the same file.Status update — 2026-05-05
Track B canonical (#39469) — open → closed 2026-04-24 as already-implemented via
e95efa4373 fix(sessions): dedupe redundant delivery mirrorsand follow-upd842ec4179 fix: tighten delivery mirror dedupe (#67185), both shipped inv2026.4.15. That fix is in the transcript-append layer (src/config/sessions/transcript.ts): before appending aprovider:"openclaw", model:"delivery-mirror"assistant message, the writer trims visible text and reuses the latest assistant entry when the trimmed text matches. Important framing: that closure addresses assistant-message-shape Track B (the duplicate-mirror symptom). It does not touch the system-event-shape lane that this issue's subject is about — system events have a different object shape and do not flow throughappendAssistantMessageToSessionTranscript. The system-event-shape lane remains unaddressed at the canonical layer.Track B canonical-adjacent PRs (state at filing → now)
e95efa4373/d842ec4179resolved the symptom at the producer layer instead of via consumer-side filtering. The audit-bypass concern flagged in review on this PR is the reason the implementation track for the system-event-shape lane (feat(events): add audience field for hidden runtime-context system events #72201, see below) avoids a consumer-side filter and uses wrap-on-drain instead.visibility.showAlertscorrection only.Adjacent reshape that affects this PR's design (not a supersession)
9180173f9a fix: preserve exec event routing and sanitize tool XML(post-v2026.5.4). IntroducesselectGenericSystemEvents+consumeSelectedSystemEventEntriesso exec-completion events stay queued for the heartbeat path and only generic events are drained at the autoreply integration point. This reshapes the surface feat(events): add audience field for hidden runtime-context system events #72201 hooks into but does not replace it — the audience split still applies to the generic events that are drained. feat(events): add audience field for hidden runtime-context system events #72201 is rebased on top of this and now usesconsumeSelectedSystemEventEntries(...)rather thandrainSystemEventEntries(...).Direct system-event-shape reports (original A list)
Cross-cuts assistant-message-shape (B)
Cross-cuts Track A — duplicate persistence (C, canonical #66443 still open)
Adjacent / overlapping FRs
Correction on prior precedent
Implementation track
feat(events): add audience field for hidden runtime-context system eventsis the live implementation slice from this proposal. Currently OPEN, rebased onto currentmain(HEADe68ce9353c). Adds optionalSystemEvent.audienceand wraps internal-audience events atdrainFormattedSystemEventsin the existingINTERNAL_RUNTIME_CONTEXT_*delimiters; migrates exactly one producer (queueCronAwarenessSystemEvent) for review tractability. No batch migration committed.Self-correction on the original "Consumer-path filter" section
That section proposed a sibling predicate to #69217's
isTranscriptOnlyOpenClawAssistantMessage. With #69217 closed without merge AND the v2026.4.15 transcript-dedup fix landing producer-side, the consumer-side predicate approach is no longer actively pursued by this issue — #72201's wrap-on-drain primitive supplants it. The original "Proposed Shape" → "Consumer-path filter" subsection is preserved below for context but should be read as superseded by the wrap-on-drain design.Why This Issue Exists
Several open reports describe system-event text landing in user-visible chat transcripts across multiple channels. The individual reports differ in surface and exact symptom, but the shared pattern is:
maybeNotifyOnExit, heartbeat relay, cron) callsenqueueSystemEventwith aSystemEventpayload.chat.history,sessions.get, sessions history HTTP, channel dispatchers) that have no way to distinguish "this event was meant for internal agent awareness" from "this event should reach the user."Existing visibility controls in the runtime (
tools.exec.notifyOnExit,visibility.showAlerts,heartbeat.target,suppressToolErrors,ANNOUNCE_SKIP_TOKEN) are each too coarse to express this distinction at event granularity. Prior work that closed similar reports by adding boolean flags (e.g., #22823, #20193) has not held up in practice — #19894 showed thatnotifyOnExit:false + suppressToolErrorsstill leaks sub-agent exec failures, which suggests that boolean notify flags are not the right primitive.The
ExecToolDefaults.triggerfield already carries'heartbeat'/'cron'/ caller context throughrunExecProcess, butmaybeNotifyOnExitnever reads it. That signal is present in the runtime today but unused for classification.Related Issues
A. System-event-shape reports (direct)
[Queued messages while agent was busy], derailing active conversation on Discord."Handle the result internally. Do not relay it to the user unless explicitly requested."directive itself — lands in visible transcript.B. Cross-cuts assistant-message-shape (addressed by #69217)
bash-tools.exec-runtime-*,heartbeat-runner-*,session-system-events-*,sessions-history-http-*as leak paths. Some symptoms should be covered by Hide transcript-only OpenClaw history artifacts #69217 once it lands; residual system-event text is this issue's responsibility. Worth re-reading with the reporter after both changes are in place.C. Cross-cuts Track A (duplicate persistence — canonical #66443)
These overlap with Track A at the persistence layer. An event-level
audienceclassification would prevent persistence for internal events, which partially addresses them from this direction; the Track A idempotency work covers the remainder.Adjacent (not addressed here)
bash-tools.exec-runtime.ts.Overlapping feature requests
defaultCompletionAudienceconfig proposed below.sessions_spawn + sessions_yieldoverexec --background. Workaround-level; would remain valid advice but the underlying reasonexec --backgroundis broken for heartbeat-invisible completion would go away.Prior attempts at this problem class (closed)
notifyOnExit:false + suppressToolErrors.tools.exec.notifyOnExit.tools.nodes.notifyOnExit.Related PRs
In-flight Track B slices
isTranscriptOnlyOpenClawAssistantMessage). Track B canonical-adjacent; this issue proposes a system-event-shape peer predicate in the same file.EXEC_INJECTION_PREFIX_RE) forSystem (untrusted):exec messages andHEARTBEAT_OKacks. Closes [Bug]: Control UI renders async exec system events in the visible chat transcript #68992 for Control UI. The author explicitly notes "fix is UI display-layer only, does not affect channel delivery" and "Did NOT verify: non-webchat channels" — so the same system events still leak on every non-Control-UI surface. Useful defense-in-depth; not a substitute for a backend fix.visibility.showAlertsinstead ofshowOk. Scoped to one misreferenced flag; does not change the abstraction.Historical precedent
tools.nodes.notifyOnExitflag.Current Working Hypotheses
1. Classification gap at the event layer
SystemEvent(src/infra/system-events.ts) hastext,ts,contextKey,deliveryContext,trusted— no field that expresses the producer's intent about whether the event should reach the user.deliveryContextroutes where an event goes; it does not say whether the event should appear on any user-facing surface.ExecToolDefaults.trigger(src/agents/bash-tools.exec-types.ts:12) already carries'heartbeat'/'cron'/ caller context through to the runtime.maybeNotifyOnExitinsrc/agents/bash-tools.exec-runtime.ts:279–308does not consult it when deciding what to emit. The signal is present, unused.2. Consumer-path write-through to visible transcripts
buildExecEventPrompt({ deliverToUser: false })insrc/infra/heartbeat-events-filter.ts:41–54produces text that literally says "Handle the result internally. Do not relay it to the user unless explicitly requested." #68508 shows this exact directive text landing in visible chat transcripts, which suggests the filter is happening at the agent-instruction level but not at the transcript-persistence level. The pattern #69217 establishes for assistant-message-shape artifacts — a shared predicate called at consumer-path boundaries — applies directly to the system-event shape as well.3. Per-surface patches are running faster than the underlying problem shrinks
The three Track B PRs filed in the last 48 hours each address a different slice (artifact shape, one UI surface, one misreferenced flag). They collectively close roughly one of the open reports in the system-event cluster. The remaining reports span Matrix, Discord, Telegram, Teams, Mattermost, TUI, and webchat; without an event-level primitive, each new surface needs its own filter and each format change (timezone prefix, text wording, output length) re-opens existing filters. An event-level
audiencefield is the layer at which this can be closed consistently.4. Boolean notify flags are structurally insufficient
Closed-issue precedent #19894 showed that
notifyOnExit:false + suppressToolErrorscompositions still leak sub-agent exec failures. Adding more top-level booleans has not held. An enum field on the event itself is a different concept, not another boolean.Proposed Shape
This is a proposal, not a committed design. Happy to revise per maintainer preference on any of these.
Core primitive
Extend
SystemEventwith an optionalaudiencefield:Default is
'auto'. No existing call site has to change.'user-facing'— reach the user when routing and visibility allow.'internal'— agent/session/log awareness only. Heartbeat may read; agent may reason; appears in session state. Does not flow to user-facing chat surfaces. Not written tochat.historyas a visible entry.'auto'— defer to downstream (current behavior).Spawn-time intent
Extend
ExecToolDefaultsandProcessSessionwithcompletionAudience?: SystemEventAudience. Derive atmaybeNotifyOnExit:This activates the existing-but-unused
triggerfield. Matches #66460's reporter's explicit fix proposal, generalized beyond cron.Relay-time honoring
In
src/infra/heartbeat-runner.ts(canRelayToUsercomputation around line 812):buildExecEventPrompt({ deliverToUser })already handles the downstream wording and does not need to change.Consumer-path filter (sibling to #69217)
Peer predicate in
src/config/sessions/transcript.ts, alongsideisTranscriptOnlyOpenClawAssistantMessage:Wired into the same consumer-path boundaries #69217 touches. The exact call-site shape — two peer predicates called per consumer, or one combined dispatcher that inspects entry shape — is a maintainer preference. Happy to match whatever #69217 settles on.
Config fallback
Optional per-channel default for unspecified events:
Backward-compat: when unset, behavior collapses to current
visibility.showAlertssemantics. Superset, not replacement. Subsumes #13911.What this does not change
notifyOnExit: falsestill short-circuits before any event is emitted.visibility.showAlerts: falsestill mutes per channel.ANNOUNCE_SKIP_TOKENforsessions.sendis orthogonal and stays.isTranscriptOnlyOpenClawAssistantMessageis untouched — peer, not replacement.showAlertscorrection is complementary.What This Issue Should Track
audienceprimitive is the right direction, versus alternatives such as extending per-channelshowAlertsto be per-trigger, or extendingtrustedto be a tri-state visibility field.audiencevsvisibilityon the event;completionAudiencevscompletionVisibilityon defaults. Draft usesaudienceto reduce conflation with the existingvisibility.showAlertschannel-level knob.Suggested Exit Criteria
This issue should not close until:
SystemEventcarries anaudiencefield and the exec runtime derives a value from trigger context where available.Notes On Issue Closure
Open issues in the list above with unique reproductions or unique affected surfaces should stay open until their exact symptom is verified fixed. This issue is meant to consolidate analysis and design, not to auto-close linked issues.
If #69366 merges and closes #68992 for Control UI only, non-Control-UI reports of the same symptom should stay open and be tracked here until the backend fix lands.
Happy to contribute an implementation PR along these lines if the direction is accepted, following the same iterative bot-review cadence as #67508.
AI-assisted research: cross-indexed open and closed issues, traced the event pipeline in
2026.4.19-beta.2, and drafted this issue with Claude Code (Claude Opus 4.7, 1M context). Fully reviewed.