Skip to content

fix(tui): keep internal runtime events out of the session peek - #5376

Open
Lstarsky0 wants to merge 3 commits into
Hmbown:mainfrom
Lstarsky0:fix/5375-peek-hides-internal-runtime-events
Open

fix(tui): keep internal runtime events out of the session peek#5376
Lstarsky0 wants to merge 3 commits into
Hmbown:mainfrom
Lstarsky0:fix/5375-peek-hides-internal-runtime-events

Conversation

@Lstarsky0

Copy link
Copy Markdown
Contributor

Closes #5375

Repro first, since it turned up something the issue doesn't mention. Building a session from the real constructors and running it through both paths:

PROJECTION waiting: role=user raw_envelope_survives=false
PROJECTION shell:   role=user raw_envelope_survives=true
PEEK kind=User text=hello
PEEK kind=User text=<codewhale:runtime_event kind="waiting_for_subagents" visibi…
PEEK kind=User text=<codewhale:runtime_event kind="background_shell_completion" …

Two separate facts. The peek doesn't use the restore projection at all, which is the reported bug. And background_shell_completion isn't recognised by the projection either — is_subagent_handoff_turn_meta matches only subagent_handoff provenance, and the shell handoff carries shell_completion.

The fix

runtime_handoff's module doc says it owns the envelope and its recognition "so creation and recognition cannot drift". The TUI restore path uses that; build_peek grew up beside it reading session.messages raw. So recognition goes in that module rather than becoming a fourth copy in the peek:

pub(crate) fn is_internal_runtime_handoff(message: &Message) -> bool

Structural, matching what runtime_handoff_message_with_meta builds — two text blocks, no cache markers, a runtime provenance line in the trailing envelope. It covers shell_completion as well; raw_runtime_handoff_text stays as it is, because it answers the narrower question of what the projection knows how to rewrite, and I'd rather not widen a function whose callers depend on that.

The provenance list stays exact — the two kinds this module emits — rather than "anything non-authoritative". imported_transcript and memory_recall are also non-authoritative and are content a peek should show.

Filtering runs before the tail is taken. Afterwards it would spend the twelve-entry budget on rows nobody sees, which in a session with busy sub-agents is most of the pane. message_count counts the same conversation the entries come from, so omitted_before + entries.len() == message_count still holds and the dashboard's two numbers still account for the rows it drew.

Deliberately not touched

TUI history renders shell completions as HistoryCell::User, and apply_loaded_session_never_restores_background_shell_event_as_composer_draft pins it — that test's concern was the composer, not the transcript. Whether the terminal should hide them too is a real question, but it's a different one from what the web pane shows, and I don't want to silently reinterpret a passing test's intent inside a web fix. Happy to open it separately if you want it.

Also not touched: the full-transcript GET /v1/sessions/{id} response. The dashboard always asks ?peek=true, and a debug surface returning everything is the "opt-in, clearly labeled" case the issue allows. Say the word if you'd rather it carried explicit provenance too.

Tests

Both directions the issue asks for, plus the accounting:

  • internal_runtime_events_are_absent_from_a_peek — every handoff shape that can reach a saved session, including the restore checkpoints a post-resume save persists
  • real_user_messages_survive_the_runtime_filter
  • a_person_who_pastes_a_runtime_envelope_is_still_the_person_talking — one block, so it stays visible; the filter keys on shape, never on the envelope text
  • runtime_traffic_does_not_spend_the_entry_budget
  • the_counters_describe_what_the_pane_can_show

Verification

cargo fmt --check clean. Workspace clippy with CI's exact allow-list: exit 0.

Full codewhale-tui --lib, same machine, two rounds on the branch plus a baseline on clean main:

passed failed
main a9acd63 10382 10
this branch, round 1 10387 10
this branch, round 2 10387 10

The failing set is byte-identical in all three — nine reasoning/thinking-ladder tests plus the tools::pdf timing flake. Not mine, and worth flagging separately: they aren't the parallel-load family from #5355. settings::reasoning_effort_setting_normalizes_and_clears fails deterministically with left: Some("xhigh"), right: Some("max"), which reads like the thinking-ladder change and its tests disagreeing rather than a race. I'll dig into that on its own rather than bundle it here.

Budget: measured, not carried over — main is at 693110 and this branch at 693289, so +179 owned Rust lines, mostly tests. Acknowledged in a separate commit, and re-measured after the rebase rather than reusing the pre-rebase number (upstream shrank 6 lines underneath it). The new ceiling is the exact measurement, not the old one plus a delta.

Analysis drafted with local tooling; every change and test verified by hand.

Runtime handoffs are persisted with `role = "user"` because strict chat
templates reject anything else mid-conversation. `runtime_handoff` says
so in its module doc and owns both the envelope and its recognition "so
creation and recognition cannot drift" — the TUI restore path uses that
recognition, `build_peek` never did, so the web dashboard rendered
`waiting_for_subagents` and `background_shell_completion` records as
rows labelled USER, contradicting the `visibility="internal"` contract
the records carry in their own text.

Recognition goes where the module says it belongs rather than becoming a
fourth copy in the peek. `is_internal_runtime_handoff` matches the
structural shape `runtime_handoff_message_with_meta` builds — two text
blocks, no cache markers, a runtime provenance line in the trailing
envelope — so someone who pastes an envelope while asking about it
submits one block and stays visible. That is the same discriminator
already keeping the restore projection off user-authored lookalikes.

It covers `shell_completion` provenance too, which `raw_runtime_handoff_text`
does not: that one answers the narrower question of what the projection
knows how to rewrite. The provenance whitelist stays exact — the two
kinds this module emits — rather than filtering everything
non-authoritative, since `imported_transcript` and `memory_recall` are
content a peek should show.

The filter runs before the tail is taken. Filtering afterwards would
spend the twelve-entry budget on rows nobody sees, which in a session
with busy sub-agents is most of the pane. `message_count` counts the
same conversation the entries come from, so the dashboard's "N messages"
and "M earlier messages not shown" still account for what it rendered.

TUI history is left alone. It renders shell completions as User cells
and `apply_loaded_session_never_restores_background_shell_event_as_composer_draft`
pins that; whether the terminal should also hide them is a separate
question from what the web pane shows.

Both directions are tested, per the issue: every handoff shape that can
reach a saved session is absent, and real user messages — including one
quoting an envelope — survive.

Closes Hmbown#5375

Analysis drafted with local tooling; every change and test verified by hand.
@Lstarsky0
Lstarsky0 requested a review from Hmbown as a code owner August 14, 2026 07:08
@github-actions

Copy link
Copy Markdown
Contributor

Thanks @Lstarsky0 for taking the time to contribute.

This repository is observing a maintainer-managed PR intake gate in dry-run mode, so this pull request is staying open. This note helps maintainers prepare the allowlist before any enforcement is considered.

Please read CONTRIBUTING.md for the expected contribution shape. A maintainer can grant recurring PR access by commenting /lgtm on a pull request.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0201aec534

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

text: turn_meta,
cache_control: meta_cache,
},
] = message.content.as_slice()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recognize handoffs that gained attachment blocks

When an idle sub-agent completion payload contains an [Attached image: …] reference, handle_idle_subagent_completion routes it through user_content_blocks, which inserts image/tag blocks or an attachment-notice block before the trailing turn_meta. This exact two-block destructure then returns false, so the peek again exposes the raw internal runtime envelope and lets it consume the entry budget. Recognize the provenance block independently of these attachment-expansion blocks while still validating the runtime envelope.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct, and I had the construction path wrong. Fixed in 591440825.

I assumed every handoff came from runtime_handoff_message_with_meta, which does build exactly two blocks. handle_idle_subagent_completion doesn't use it — it takes only subagent_completion_runtime_text / subagent_failure_runtime_text and sends that through handle_send_message with SubAgentHandoff provenance, so the message is assembled by user_content_blocks like any other turn.

Reproduced before changing anything, with a real waiting_for_subagents envelope and the blocks that path inserts:

recognized=false
PEEK User hi
PEEK User <codewhale:runtime_event kind="waiting_for_subagents" visibility="inte…

Worth adding to your description: it isn't only the success case. When attach_image_from_path fails, notice_block still pushes a block, so a payload that merely mentions an attachment breaks the pair whether or not the file loads.

The predicate now anchors on the first and last block with [Text, .., Text]. I did not add an envelope-text check alongside it — recognition keying on payload text is what the module deliberately avoids, and the provenance line already does the separating: a composer turn is ExternalUser, whose authority is implicit, so turn_metadata omits the provenance line for it entirely. Nothing a person sends can carry subagent_handoff or shell_completion, at any block count. The paste test now covers the multi-block shape too, not just the single-block one.

Verified rather than assumed: reverting the .. back to an exact pair makes the new test fail with the envelope showing up as a peek entry, and restoring it passes. Full codewhale-tui --lib is two rounds at 10 failures, the same set main fails on CI plus one local tools::pdf — no change from this branch.

Same hole exists in raw_runtime_handoff_text, which the restore projection uses, so an idle completion with an attachment also replays raw into a resumed thread. Left alone here: that one gates a rewrite rather than a hide, and widening it is a separate change with its own blast radius. Happy to send it if you want it.

@Lstarsky0

Copy link
Copy Markdown
Contributor Author

The macOS and Windows legs here are red, and they aren't mine — filed as #5377.

Short version: main itself is red on both platforms right now. Its own run 31778394006 on c9b9559a7 fails the same nine reasoning-effort tests this branch does, and they bisect to 6f6c35183 (thinking ladder), three commits before I branched. The failing set from main's log is identical to this PR's.

Worth knowing for reading the checks list here: Test (ubuntu-latest) passes in five seconds on both. Linux workspace tests moved to CNB, so on a pull_request event that leg preserves the required context without running anything. macOS and Windows are the legs that ran the suite.

Nothing in this diff reaches those tests — it touches session_peek and one predicate in runtime_handoff. Locally, same machine: main 10 failures, this branch 10, identical set, the extra one being a tools::pdf flake that is local-only and not in CI's set.

Happy to rebase once #5377 lands, or if you'd rather see this one green first I can send the test re-pin as its own PR and this can go on top.

The exact-pair destructure was wrong about how these messages are built.
Idle sub-agent completions do not go through
`runtime_handoff_message_with_meta` at all: `handle_idle_subagent_completion`
takes only the envelope *text* and sends it through the engine's ordinary
path, where `user_content_blocks` expands any `[Attached image: …]` line in
the payload into image blocks — or a notice block when the file fails to
load, so the block count grows either way. Between the envelope and its
provenance marker, that leaves a message the predicate did not match, and
the peek rendered the raw envelope as the person's own message and spent an
entry on it.

Anchor on the first and last block instead. The provenance line is what
actually separates runtime traffic from a person: a composer turn is
`ExternalUser`, whose authority is implicit, so `turn_metadata` omits the
provenance line entirely for it. Nothing a person sends is matched by this
regardless of how many blocks it has, which the paste test now covers in
the multi-block shape as well as the single-block one.

Found by the Codex reviewer on Hmbown#5376.

Analysis drafted with local tooling; every change and test verified by hand.
Analysis drafted with local tooling; every change and test verified by hand.
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.

codewhale web: saved-session peek renders internal runtime events as user messages

1 participant